浏览器同源策略

本文最后更新于:8 个月前

什么是同源策略

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

URL 说明 是否通讯
http://www.abc.com/a.js
http://www.abc.com/b.js
相同域名下的不同文件 ok
http://www.abc.com:9090/a.js
http://www.abc.com:2020/a.js
不同的端口 no
http://www.abc.com/a.js
http://www.abd.com/a.js
不同域名 no
http://www.abc.com/a.js
https://www.abc.com/a.js
不同协议 no
http://b.abc.com/a.js
https://www.abc.com/a.js
主域名不同 no

  简而言之就是必须要协议域名端口完全相同才不会触发非同源策略,不然都是属于跨域的,那么针对这种情况,我们可以有以下几种解决的方法。

1、jsonp

因为浏览器中有三种标签天生就支持跨域<img><script><link>jsonp就是利用script标签的性质,通过script传输JSON数据。因为此方法比较简单,而且可以兼容性强,所以简单解决跨域问题是可以的,但是这种方法有很大的问题,因为是script标签传输JSON,所以:

  • 只能支持get方法
  • 不安全,可能会受到XSS的攻击

  我在自己的服务器上搭建了一个简单的接口,返回一串callback(‘测试数据’)的字符串,可以实现跨域请求数据。

function jsonp({
    url,
    callback
    }) {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        window[callback] = function(data) {
            resolve(data)
            document.body.removeChild(script)
        }
        script.src = `${url}?callback=${callback}`
        document.body.appendChild(script)
        })
    }
jsonp({
    url: 'http://xxx.xx.xx.xx:7777/api/test',
    callback: 'show'
    }).then(data => {
        console.log(data)
    })

2、document.domain + iframe

当仅仅只有子域不同时,我们只需要在js中强制设置document.domain为基础主域,就可以实现跨域

//a.xxx.com/a.html
<iframe id='iframe' src='http://b.xxx.com/b.html' style='display:none;'></iframe>
<script>
    document.domain = 'xxx.com';
    var user = 'admin';
</script>
//b.xxx.com/b.html
<script>
    document.domain = 'xxx.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

其他的如location.hash,window.name 都是类似的方法,但是个人觉得不好用,所以就略过吧。

3、postMessage

  window.postMessage()方法提供了一种可控制的机制来安全地规避同源策略,安全地实现了Window对象之间的跨域通信。例如,在页面与其产生的弹出窗口之间,或在页面与嵌入其中的iframe之间。
  大致上来说,一个窗口可以获取对另一个窗口的引用,例如:通过targetWindow = window.opener来获取另一个页面的对象,然后使用来MessageEvent在其上分派一个targetWindow.postMessage(),然后,接收窗口可以根据自身需要自由的处理此事件。传递给事件的参数window.postMessage()(即“消息”)通过事件对象暴露给接收窗口。

//9090
 var targetWindow = window.open('http://xxx.xxx.com:9090');
 targetWindow.posetMessage('hello,i am 9090','http://xxx.xxx.com:9091');
 targetWindow.addEventListener('event',e=>{
     if(e.origin!=='https://xxx.xxx.com:9090')
     return
     console.log(e.data) // i am 9091
 },false)
//9091
window.addEventListener("message", (event) => {
  if (event.origin !== "http://xxx.xxx.com:9090")
    return;
  console.log(e.data) // hello,i am 9090
  event.source.postMessage('i am 9091',event.origin);
}, false);

4、CORS

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

  整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
 只要同时满足以下两大条件,就属于简单请求。

  1. 请求方法是以下三种方法之一:HEADGETPOST
  2. HTTP的头信息不超出以下几种字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(其中Content-Type只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain
    这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。**凡是不同时满足上面两个条件,就属于非简单请求**
    浏览器对这两种请求的处理,是不一样的。

    简单请求

      浏览器发送简单请求时,会自动在头上加上Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
  • 如果服务器Access-Control-Allow-Origin规定的许可范围内没有这个值,会返回一个头信息中不包含Access-Control-Allow-Origin字段的响应,此时浏览器会检测到错误并抛出错误。此错误不能从状态码中发现。

  • 如果在许可范围内,服务器返回的响应中会多处几个头字段

    • 1)Access-Control-Allow-Origin

    该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

    • 2)Access-Control-Allow-Credentials

    该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

    • 3)Access-Control-Expose-Headers

    该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

    想要通过CORS发送Cookie,想要同时在服务器设置Access-Control-Allow-Credentials字段为true,另一方面,开发者必须在AJAX请求中打开withCredentials属性。

    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;

    同时如果要发送CookieAccess-Control-Allow-Origin就不能设为*,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie

    非简单请求

      浏览器发现这是一个非简单请求时,会自动发出一个预检请求,要求服务器确认可以这样请求。预检请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
    其中,”预检”请求的头信息包括两个特殊字段:
    (1)Access-Control-Request-Method
      该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,如PUTDELETE
    (2)Access-Control-Request-Headers
      该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段
    服务器收到请求后,会先进行OriginAccess-Control-Request-MethodAccess-Control-Request-Headers的检查,确认是否是可以跨域的请求:

  • 如果检查没问题,就会返回添加了相应头字段的响应

  • 如果服务器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获,在控制台报错。
    其中正确返回的响应的头字段会加入一下几个字段:
    (1)Access-Control-Allow-Methods
      该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
    (2)Access-Control-Allow-Headers
      如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
    (3)Access-Control-Allow-Credentials
      该字段与简单请求时的含义一样都是是否允许发送Cookie
    (4)Access-Control-Max-Age
      该字段可选,用来指定本次预检请求的有效期,单位为秒。比如缓存时间设置为1728000秒(20天),那么在此期间,都不用发出另一条预检请求,此期间之前的预检请求会存活直到期。

    5、服务器中转

    因为跨域是浏览器行为,不是服务器行为,所以实际上每次跨域请求都是到达服务器了的,只不过在回来的时候被浏览器限制了。所以我们可以通过服务器去实现访问,然后把数据从我们自己的同源中请求到。

    nginx代理

      通过nginx代理我们要访问的地址,借助中转服务器去访问

    // proxy服务器
    server {
        listen       80;
        server_name  www.my.com;
        location /api {
            //移除api
            rewrite ^/api/(.*)$ /&1 break;
            proxy_pass   http://www.target.com;  #反向代理
            proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        }
    }

    6、nodejs中间件代理

      node做一层中间代理服务器,首先node接收接受客户端请求,然后将请求转发给目标服务器,node拿到目标服务器的响应数据,之后node将响应结果发给客户端,至此完成跨域请求。

    7、WebSocker协议

    WebSockerHTML5的一种持久化协议,可以在服务端和客户端建立起双向的通信通道,实现持久的数据通信,一般都是用来做通信聊天的,既然可以进行数据交互,我们拿来跨域也是可以的,不过就是不方便而已啦

  综上,就这么几种解决跨域的方法,总结一下他们的差异:

  1. jsonp
    需要目标服务器配合一个callback函数 
  2. window.name+iframe
    需要目标服务器响应window.name
  3. window.location.hash+iframe
    同样需要目标服务器作处理
  4. html5的postMessage+ifrme
    这个也是需要目标服务器或者说是目标页面写一个postMessage,主要侧重于前端通讯
  5. CORS
    需要服务器设置header :Access-Control-Allow-Origin
  6. 服务器中转
    nginx反向代理或node代理中间层 这个方法一般很少有人提及,但是他可以不用目标服务器配合,不过需要你搭建一个中转nginx服务器,用于转发请求
  7. WebSocker 双方都需要实现WebSocker,也不太方便
    所以我还是比较喜欢CORS和中转的方法的。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!