跨域那些事儿

831 阅读9分钟

为什么会出现跨域问题?因为浏览器的同源策略限制。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

一 同源策略

1.1 同源的定义

同源策略/SOP(Same origin policy),所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个 ip 地址,也不是同源。
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

1.2 源的继承

在页面中通过 about:blankjavascript: URL 执行的脚本会继承打开该 URL 的文档的源,因为这些类型的 URLs 没有包含源服务器的相关信息。

例如,about:blank 通常作为父脚本写入内容的新的空白弹出窗口的 URL(例如,通过  Window.open()  )。 如果此弹出窗口也包含 JavaScript,则该脚本将从创建它的脚本那里继承对应的源。

1.3 源的更改

脚本可以将 document.domain 的值设置为其当前域或其当前域的父域

端口号是由浏览器另行检查的。任何对document.domain的赋值操作,包括 document.domain = document.domain 都会导致端口号被重写为 null 。因此 company.com:8080 不能仅通过设置 document.domain = "company.com" 来与company.com 通信。必须在他们双方中都进行赋值,以确保端口号都为 null 。 (试了,两个不同端口的页面设置document.domain=document.domain之后,Cookie共享了。。但是不知道怎么恢复。)

1.4 跨源网络访问

同源策略控制不同源之间的交互,例如在使用XMLHttpRequest<img> 标签时则会受到同源策略的约束。这些交互通常分为三类:

  • 跨域_写操作(Cross-origin writes)一般是被允许的。_例如链接(links),重定向以及表单提交。特定少数的HTTP请求需要添加 preflight(预检请求)。
  • 跨域_资源嵌入(Cross-origin embedding)_一般是被允许。
  • 跨域_读操作(Cross-origin reads)一般是不被允许的,_但常可以通过内嵌资源来巧妙的进行读取访问。例如,你可以读取嵌入图片的高度和宽度,调用内嵌脚本的方法,或availability of an embedded resource.

1.5 限制范围

  1. Cookie、LocalStorage 和 IndexDB 无法读取。
  2. DOM 无法获得。
  3. AJAX 请求不能发送。


如何允许跨源访问?
可以使用 CORS 来允许跨源访问。CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以加载资源。

二 如何跨域

2.1 CROS 跨域资源共享

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器  让一个源(域)上的Web应用可以访问来自不同源服务器上的资源。

功能概述

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequestFetch )使用 CORS,以降低跨域 HTTP 请求所带来的风险。

CORS 访问控制

简单请求

某些请求不会触发 CORS 预检请求,称为“简单请求”(非规范定义):

预检请求

除了简单上面的简单请求,跨域请求需要先使用 OPTIONS 方法发起一个预检请求,以获取服务器是否允许发起实际请求。
比如下面的请求:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';

function callOtherDomain() {
  if(invocation) {
    invocation.open('POST', url, true);
    invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
    invocation.setRequestHeader('Content-Type', 'application/xml');
    invocation.onreadystatechange = handler;
    invocation.send(body); 
  }
}


预检请求中:
首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。
首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHERContent-Type。服务器据此决定,该实际请求是否被允许。

预检请求响应中:
首部字段 Access-Control-Allow-Methods 表明允许使用 POST, GET 和 OPTIONS 方法发起请求。
首部字段 Access-Control-Allow-Headers 表明允许请求中携带字段 X-PINGOTHER 与 Content-Type。
最后,首部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒。有效时间内,浏览器无须为同一请求再次发起预检请求。

附带身份凭证的请求

一般而言,对于跨域 XMLHttpRequestFetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要将 XMLHttpRequestwithCredentials 标志设置为 true ,从而向服务器发送 Cookies。如果是需要预检的请求,需要在响应中设置 Access-Control-Allow-Credentials: true

对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。

2.2 JSONP

JSONP是利用script标签不受跨域限制而形成的一种方案。

原理

往html里插入一个

function jsonp({url, param, cb}){
  return new Promise((resolve, reject)=>{
    let script = document.createElement('script')
    window[cb] = function(data){
      resolve(data);
      document.body.removeChild(script)
    }
    params = {...params, cb}
    let arrs = [];
    for(let key in params){
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}

缺点

只支持 GET 请求,不支持 POSTPUTDELETE 等;不安全,容易受 XSS 攻击。

2.3 postMessage

window.postMessage() 方法可以安全地实现跨源通信。

语法

/**
 * @param {any} message 将要发送到其他 window的数据
 * @param {string} targetOrigin 指定哪些窗口能接收到消息事件,其值可以是字符串"*"
 *                              (表示无限制)或者一个URI。
 * @param {Transferable} transfer 要转移的 Transferable 对象
 */
otherWindow.postMessage(message, targetOrigin, [transfer]);

发送消息

var frame = document.querySelector('frame');
// 往内嵌的window发送消息
frame.contentWindow.postMessage('any type msg', 'http://iframe.example.com:8080/');
// 也可以用onmessage来监听返回的消息
window.onmessage = function(event) {
  console.log(event.data);
};

监听消息

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
  // event.origin 调用postMessage时的targetOrigin参数或者消息发送方窗口的origin
  if (event.origin !== "http://example.com:8080")
    return;
	
  // event.data 从其他 window 中传递过来的对象(postMessage中的message)。
  console.log(event.data);
  // event.source 对发送消息的窗口对象的引用
  event.source.postMessage('received your msg.', event.origin);
  // ...
}

2.4 window.name

这种方法就离谱。
原理:在同一个window内,无论URL怎么变,都不影响window.name的值。
实现:Web应用A、B同源,C不同源,要把C传给A。C中会设置window.name,A中有一个ifame,先加载C,onload之后会设置window.name,然后把iframe的src指向B,B就可以获得window.name,而A、B同源,没有跨域问题。

2.5 location.hash

依然离谱。

location.hash 特性

hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)。代表网页中的一个位置。

  • HTTP请求不包括#
  • 改变hash不触发网页重载
  • 改变hash会改变浏览器的访问历史
  • onhashchange事件,HTML 5新增,(IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。)

实现原理

image.png

2.6 Nginx反向代理

反向代理的原理就是将前端的地址和后端的地址用nginx转发到同一个地址下。
命令:

  • start nginx 在nginx目录下启动nginx
  • nginx -s reload 重启nginx

客户端解决跨域NginxNginx配置

server
{
   listen 3003;
   server_name localhost;
   ##  = /表示精确匹配路径为/的url,真实访问为http://localhost:5500
   location = / {
       proxy_pass http://localhost:5500;
   }
   ##  /no 表示以/no开头的url,包括/no1,no/son,或者no/son/grandson
   ##  真实访问为http://localhost:5500/no开头的url
   ##  若 proxy_pass最后为/ 如http://localhost:3000/;匹配/no/son,则真实匹配为http://localhost:3000/son
   location /no {
       proxy_pass http://localhost:3000;
   }
   ##  /ok/表示精确匹配以ok开头的url,/ok2是匹配不到的,/ok/son则可以
   location /ok/ {
       proxy_pass http://localhost:3000;
   }
}

服务端解决跨域NginxNginx配置

server
{
    listen 3002;
    server_name localhost;
    location /ok {
        proxy_pass http://localhost:3000;

        #   指定允许跨域的方法,*代表所有
        add_header Access-Control-Allow-Methods *;

        #   预检命令的缓存,如果不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        #   带cookie请求需要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        #   表示允许这个域跨域调用(客户端发送请求的域名和端口) 
        #   $http_origin动态获取请求客户端请求的域   不用*的原因是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        #   表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        #   OPTIONS预检命令,预检命令通过时才发送请求
        #   检查请求的类型是不是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}

这样可以实现前端页面和接口不同域名。

详细解析看参考资料[6]。

2.7 http-proxy-middleware

Vue框架利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于 Vue 渲染服务和接口代理服务都是 webpack-dev-server,所以页面与代理接口之间不再跨域,无须设置 Headers 跨域信息了。

NodeJS 中间件 http-proxy-middleware 实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中的域名,实现当前域的 cookie 写入,方便接口登录认证。

三 跨源文档API的访问

浏览器的api中,允许文档间互相引用,如 iframe.contentWindowwindow.parentwindow.open()window.opener,这些api可以拿到其他文档的对象的引用,但是当两个文档不同源时,对该对象(如Window、Location)的访问就会有所限制。如果想要两个不同源的窗口进一步交流可以使用window.postMessage。

参考资料