跨域原理及解决方案

4,592 阅读8分钟

前言

跨域是什么,为什么会有跨域?跨域的解决方法是什么?常用的是什么?原理是什么?

什么是跨域

跨域是指从一个域名去请求另一个域名的资源。

严格来说,只要域名,协议,端口任何一个不同,就视为跨域。

跨域场景

以下这种看上去再相似也没有用,都是跨域。

主域不同

http://www.chrome.cn/index.html
http://www.chomper.cn/server.php

子域名不同

http://abc.chomper.cn/index.html 
http://def.chomper.cn/server.php

端口不同

http://www.chomper.cn:8080/index.html
http://www.chomper.cn/server.php  

协议不同

https://www.chomper.cn/index.html
http://www.chomper.cn/server.php

localhost 调用 127.0.0.1 也属于跨域

非跨域场景

以下协议、域名、端口一致,所以是非跨域。

http://www.chomper.cn/index.html 
http://www.chomper.cn/server.php

跨域提示

当跨域时会收到以下错误

为什么会出现跨域

为了网络安全起见,浏览器设置了同源策略,规定只有域名,端口,协议全部相同,就叫做同源。

如何解决跨域

跨域资源共享

跨域资源共享 CORS 是一种机制,准确的说是一个 W3C 标准,可以克服同源策略的限制,另外 CORS 是目前主流的跨域解决方案。

整个资源共享过程,不需要开发者参与,都是浏览器自动完成,对于开发者来说,同源或是非同源的异步通信是没有差别的,代码完全一样。

过程中,浏览器一旦发现异步请求跨源,就会自动添加一些附加的头信息,因此,实现资源共享的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

浏览器在发送跨域请求的时候,会先判断下是 简单请求 还是 非简单请求,因为浏览器对这两种请求方式的处理方式是不同的。

简单请求

如果是 简单请求,就先执行服务端程序,然后浏览器才会判断是否跨域。

浏览器会在头信息中,增加一个 Origin 字段(协议 + 域名 + 端口),服务器根据 Origin ,决定是否同意这次请求。

如果不在许可范围内,服务器会返回一个正常的 HTTP 回应,浏览器发现,这个回应的头信息中没有包含 Access-Control-Allow-Origin 字段,就会抛出一个错误。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

只要满足下列几种情况,就是 简单请求,反之为 非简单请求

情况一,使用以下请求方式为简单请求,其他请求均为非简单请求。

  • GET
  • POST
  • HEAD

情况二,设置头部信息不能超过以下几种字段。

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (需要注意额外的限制)
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

情况三,Content-Type 的值仅限于下列三者之一 (例如 application/json 为非简单请求)。

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

非简单请求

除以上情况外,就是 非简单请求

如果是非简单请求,浏览器会先发送 OPTIONS 请求,进行预检,这一次的请求称为“预检请求”,服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。

浏览器会在发送 OPTIONS 请求时会自动添加 Origin 字段,还会包括两个特殊字段。

Access-Control-Request-Method  (会用到哪些 HTTP 方法)
Access-Control-Request-Headers (一个逗号分隔的字符串,指定额外发送的头信息字段)

服务器响应 OPTIONS 请求,会在响应头里添加 Allow-Origin Allow-Methods Allow-Headers ,如果服务器的 OPTIONS 响应不合你的要求,你可以手动在服务器配置 OPTIONS 响应,以应对带预检的跨域请求。

注意: 服务端可以通过设置 Access-Control-Max-Age 告诉浏览器在一定时间内无需再次发送预检请求,但是如果浏览器禁用缓存则无效。

浏览器收到 OPTIONS 响应,会比较真实请求的 method 是否属于返回的 Allow-Methods 的值之一,还有 origin , head 也会进行比较是否匹配。

注意:如果通过,浏览器就继续向服务器发送真实请求,否则就会报预检错误。

服务端处理

由于出现跨域的问题,所以服务端需要设置同意任意跨源请求:

Access-Control-Allow-Origin: *

Allow-MethodsAllow-Headers 只在响应 options 请求时有作用,Allow-Origin 在响应 options 请求和响应真实请求时都是有作用的,两者必须同时包含要跨域的源。

虽然可以通过设置响应头和响应方式等支持非简单请求,但是不到万不得已的情况,不能允许客户端发送非简单请求。因为非简单请求会使服务器比简单请求的多一倍的压力。

最后,当你使用 IE<=9, Opera<12, or Firefox<3.5 或者更加老的浏览器,这个时候请使用 JSONP 。

关于cookie

CORS 请求默认不发送 Cookie,想要传递 Cookie 需要满足 3 个条件:

  1. Access-Control-Allow-Credentialstrue,代表服务器同意发送 Cookie
// 原生 xml 的设置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 设置方式
axios.defaults.withCredentials = true;
  1. 请求必须设置 withCredentials,否则,即便服务器要求设置 Cookie,浏览器也不会处理。
  2. Access-Control-Allow-Origin 为非 *,须指定明确的、与请求网页一致的域名。

浏览器看不到 options

在新版的 chrome 中,如果你发送了复杂请求,你却看不到 options 请求。可以在这里设置 chrome://flags/#out-of-blink-cors 设置成 disbale ,重启浏览器。对于非简单请求就能看到 options 请求了。

JSONP

由于 script 标签从不同域名下加载静态资源文件是被浏览器允许的,所以 JSONP 就是利用了这个“犯罪漏洞”来进行跨域。

虽然这种方式非常好用,但是最大的缺陷就是,仅支持 GET 方法,如果想使用完整的 REST 接口,请使用 CORS 或者其他代理方式。

动态创建 script 标签

let script = document.createElement('script');

script.src = 'http://www.chomper.cn/login?username=chomper&callback=callback';

document.body.appendChild(script);

function callback(res) {
  console.log(res);
}

普通 js 示例

<script type="text/javascript">
  window.jsonpCallback = function(res) {
    console.log(res);
  };
</script>
<script
  src="http://localhost:8080/api/jsonp?msg=hello&cb=jsonpCallback"
  type="text/javascript">
</script>

nginx代理跨域

nginx反向代理

简单理解,就是为了解决跨域问题,先让浏览器将请求发送给当前域名下的 nginx ,然后 nginx 判断 url ,通过匹配发现是动态请求,最后在将请求转发给服务端接口,这样就绕过了浏览器,所以也不会有跨域的问题。

server {
    listen  8080;
    server_name  localhost;
    location / {
      root html;
      index index.html index.htm;
    }
    //一般只需要在该处添加代理配置信息
    location /api/ {
      rewrite  ^.+/api/?(.*)$ /$1 break;
      include  uwsgi_params;
      proxy_pass  http://10.10.10.10:1000/;
    } 
}

nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location ~* \.(eot|otf|ttf|woff|svg)$ {
  add_header  Access-Control-Allow-Origin *;
}

vue-cli 反向代理

其实思路和上面的 nginx 反向代理是一样的,先将请求拦截在中间层,在通过中间层将请求发送出去,不经过浏览器,自然就绕过了同源策略。

  devServer: {
        open: true, //是否自动弹出浏览器页面
        host: "localhost", 
        port: '8081',
        https: false,
        hotOnly: false, 
        proxy: {
            '/api': {
                target: 'http://localhost:5000', //API服务器的地址
                ws: true,  //代理websockets
                changeOrigin: true, // 虚拟的站点需要更管origin
                pathRewrite: {   //重写路径 比如 '/api/aaa/ccc' 重写为 '/aaa/ccc'
                    '^/api': ''
                }
            }
        },
    }

注意:需要匹配的接口字段设置成开发环境下的变量,在利用axios请求时设置url,在url前统一加上变量api,实现请求匹配代理。

postMessage

这是由 H5 提出来的一个 API ,它的作用就是向外界窗口发送信息,我们可以采用这种方式来规避同源策略的限制。

它可以做哪些事情:

  1. 页面和其打开的新窗口的数据传递

  2. 多窗口之间消息传递

  3. 页面与嵌套的 iframe 消息传递

otherWindow.postMessage(message,targetOrigin);

otherWindow 指的是目标窗口,也就是要给哪一个 window 发送消息, Message 是要发送的消息,类型为 StringObject(IE8、9不支持Obj)targetOrigin 是限定消息接受范围,不限制就用星号 *

详细用法看 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

WebSocket

WebSocket 本质上没有使用 HTTP, 因此也没有跨域的限制,不过笔者实在是没有用过,这里就不描述了。

document.domain + Iframe

这种跨域的方式最主要的是要求主域名相同,比如 a.test.com 和 b.test.com,这两个个主域名都是 test.com , 而主域名不同的就不能用此方法。

// a.test.com
<body>
  helloa
  <iframe
    src="http://b.test.com/b.html"
    frameborder="0"
    onload="load()"
    id="frame"
  ></iframe>
  <script>
    document.domain = "test.com";
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>
// b.test.com
<body>
  hellob
  <script>
    document.domain = "test.com";
    var a = 100;
  </script>
</body>

浏览器关闭同源安全策略

为了方便本地开发,我们可以将浏览器的同源策略关闭。

需要提醒各位,一般情况千万别轻易使用这个方式。

mac

~/Downloads/chrome-data 这个目录可以自定义.

/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary  --disable-web-security --user-data-dir=~/Downloads/chrome-data  

windows

找到你安装的目录
.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx

参考文档

https://mp.weixin.qq.com/s/vuIjDVQ0MzT6SRhVhlfswQ

https://juejin.cn/post/6844903521163182088#heading-14

https://www.jianshu.com/p/ebd498cc3c52

https://www.jianshu.com/p/c68d404a3ab9

结尾

如果这篇文章帮助到了你,欢迎点赞和关注,搜索《海洋里的魔鬼鱼》加入我们的技术群一起学习讨论,共同探索前端的边界。