Skip to content

实现跨域的方式 #6

Open
Open
@TieMuZhen

Description

@TieMuZhen

什么是跨域

在实现跨域之前先了解什么是跨域

跨域资源共享(Cross-origin resource sharing,CORS),避开浏览器的同源策略,用于让网页的受限资源能够被其他域名的页面访问的一种机制。

那什么是同源策略呢?

同源策略是指在Web浏览器中,允许某个网页脚本访问另一个网页的数据,但前提是这两个网页必须有相同的协议、域名和端口号,一旦两个网站满足上述条件,这两个网站就被认定为具有相同来源。此策略可防止某个网页上的恶意脚本通过该页面的文档对象模型访问另一网页上的敏感数据。

为什么要有跨域

Ajax 的同源策略主要是为了防止CSRF(跨站请求伪造) 攻击,如果没有 AJAX 同源策略,相当危险,我们发起的每一次 HTTP 请求都会带上请求地址对应的 cookie,那么可以做如下攻击:

1、用户登录了自己的银行页面 mybank.com,mybank.com向用户的cookie中添加用户标识。
2、用户浏览了恶意页面 evil.com。执行了页面中的恶意AJAX请求代码。
3、evil.com向http://mybank.com发起AJAX HTTP请求,请求会默认把http://mybank.com对应cookie也同时发送过去。
4、银行页面从发送的cookie中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了。
5、而且由于Ajax在后台执行,用户无法感知这一过程。

DOM同源策略也一样,如果iframe之间可以跨域访问,可以这样攻击:

1、做一个假网站,里面用iframe嵌套一个银行网站 mybank.com。
2、把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
3、这时如果用户输入账号密码,我们的主网站可以跨域访问到http://mybank.com的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。

所以说有了跨域跨域限制之后,我们才能更安全的上网了。

跨域请求到底有没有正常发出去并收到响应

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?

因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

实现跨域的方式

1、JSONP实现跨域

原理:

利用了src属性可以跨域的特性

<script src="URL">

URL

  • 绝对 URL - 指向另一个网站(比如 src="http://www.baidu.com/a.js"
  • 相对 URL - 指向网站内的一个文件(比如 src="/scripts/a.js"

除了scriptimgiframesrc属性也不受同源策略影响。

缺点:

  • 只能处理get请求
  • 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
  • jsonp在调用失败的时候不会返回各种HTTP状态码。

请求流程:

image

源码实现

function jsonp ({url, params, cb = 'callback'}) {
    return new Promise((resolve, reject) => {
        params[cb] = cb;
        // 创建script标签
        const script = document.createElement('script');
        //回调函数加在请求地址上
        const result = [];
        for (let key in params) {
            result.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${result.join('&')}`;
        document.body.appendChild(script);

        window[callback] = function (data) {
            resolve(data);
            // 回调执行完后,删除添加的script标签
            document.body.removeChild(script);
        }
    });
}

使用

jsonp({
    url: 'http://localhost:3000/show',
    params: {
        //code
    }
}).then(data => {
    console.log(data);
});

服务端代码(node):

let express = require('express')
let app = express();

app.get('/show', (req, res) => {
    let {callback} = req.query;
    let data = 'hello'; // 要返回的数据
    res.send(`${callback}(${data})`);
});
app.listen(3000);

2、CORS解决跨域(开发中最常用,安全性高,主要靠在服务端设置)

CORS即“跨域资源共享”,这是一种最常用的跨域实现方式,一般需要后端人员在处理请求数据的时候,添加允许跨域的相关请求头信息。大致思路是这样的:首先获取请求对象的信息,比如Origin字段,通过预先配置的参数判断请求是否合法,然后设置相应对象response的头信息,实现跨域资源请求。

配置代码

app.use(async (ctx, next) => {
    const origin = ctx.request.headers.origin;
    if([ // 允许跨域的白名单
        "http://www.baidu.com",
        "http://www.taobao.com"
    ].includes(origin)) {
        ctx.set("Access-Control-Allow-Origin", `${origin}`); 
        ctx.set("Access-Control-Allow-Methods", 'OPTIONS, GET, PUT, POST, DELETE');
        ctx.set("Access-Control-Allow-Credentials", "true");
        ctx.set("Access-Control-Allow-Headers", 'x-resquested-with, accept, origin, content-type');
        if (ctx.method == 'OPTIONS') {
            ctx.body = '';
            ctx.status = 204;
        } else {
            await next()
        }
    } else {
        await next();
    }
});

具体每个字段详细意义,请移步HTTP Headers

实际上浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求

简单请求是指满足以下条件的(一般只考虑前面两个条件即可):

1、使用GETPOSTHEAD 其中一种请求方法。
2、HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

3、请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;
4、XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。请求中没有使用 ReadableStream 对象。

对于简单请求,浏览器直接发起 CORS 请求,具体来说就是服务器端会根据请求头信息中的 origin 字段(包括了协议 + 域名 + 端口),来决定是否同意这次请求。
如果 origin 指定的源在许可范围内,服务器返回的响应,会多出几个头信息字段:

Access-Control-Allow-Origin: http://xxx.xxx.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

非简单请求

非简单请求时指那些对服务器有特殊要求的请求,比如请求方法是put delete,或者 content-type 的类型是 application/json。其实简单请求之外的都是非简单请求了

非简单请求的 CORS 请求,会在正式通信之前,使用 OPTIONS 方法发起一个预检(preflight)请求到服务器,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

下面是一个预检请求的头部:

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样了。

3、webSocket实现跨域

原理:

webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信。

4、Nginx实现跨域

原理

Nginx反向代理实现跨域

Nginx配置

// 把 www.a.cn的请求代理到 www.b.com

// nginx配置如下
server {
    listen 80;
    server_name www.a.cn;
    location / {
        proxy_pass www.b.com;
    }
}

5、webpack实现跨域

原理

webpack是使用webpack-dev-server插件实现跨域的,webpack-dev-server是使用http-proxy-middleware来实现跨域代理的,http-proxy-middleware库借助于node-http-proxy,用于将node服务器接收到的请求转发到目标服务器,实现代理服务器的功能。
可以推想,使用node-http-proxy创建代理服务器proxyServer后,通过全局注册的转发规则获取到客户端请求 req 需要发送到的目标地址。

跨域配置

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://www.baidu.com/',
        pathRewrite: {'^/api' : ''},
        changeOrigin: true,     // target是域名的话,需要这个参数,
        secure: false,          // 设置支持https协议的代理
      },
      '/api2': {
          .....
      }
    }
  }
};

配置中主要的参数说明

  • '/api'

捕获API的标志,如果API中有这个字符串,那么就开始匹配代理,
比如API请求/api/users, 会被代理到请求http://www.baidu.com/api/users

  • target

代理的API地址,就是需要跨域的API地址。
地址可以是域名,如:http://www.baidu.com
也可以是IP地址:http://127.0.0.1:3000
如果是域名需要额外添加一个参数changeOrigin: true,否则会代理失败。

  • pathRewrite

路径重写,也就是说会修改最终请求的API路径。
比如访问的API路径:/api/users,
设置pathRewrite: {'^/api' : ''},后,
最终代理访问的路径:http://www.baidu.com/users
这个参数的目的是给代理命名后,在访问时把命名删除掉。

  • changeOrigin

让target参数是域名。

  • secure

secure: false,不检查安全问题。
设置后,可以接受运行在HTTPS上,可以使用无效证书的后端服务器。

6、window.postMessage

window.postMessage方法,可以实现跨文档、多窗口、跨域消息的传递。

postMessage(data,origin)方法

  • data: 要传递的数据
  • origin: 字符串参数,只目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写
  • 如果要传递给所有窗口,值可以为"*"
  • 如果传给当前窗口同源的话,值可以为"/"

举例说明

先起两个服务,a.html起在localhost:3000上,b.html起在localhost:4000

// a.html
<script type="text/javascript">
     function load(){
         let frame = document.querySelector('#frame');
         frame.contentWindow.postMessage('我是a.html', 'http://localhost:4000')//向端口为4000的域发送内容"我是a.html"
}
</script>
//b.html
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
  if (event.origin !== "'http://localhost:4000'") return;
  console.log(event.data); //打印接收到的信息
}

event对象有三个属性,分别是origindatasource

  • event.data表示接收到的消息;
  • event.origin表示postMessage的发送来源,包括协议,域名和端口;
  • event.source表示发送消息的窗口对象的引用; 我们可以用这个引用来建立两个不同来源的窗口之间的双向通信。

参考文章

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions