对跨域的相关总结

203 阅读14分钟

1.什么是跨域

  • 同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策,所谓"同源"指的是"三个相同",协议相同、端口相同、域名相同
URL                      说明       是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js     同一域名下   允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js 同一域名下不同文件夹 允许
http://www.a.com:8000/a.js
http://www.a.com/b.js     同一域名,不同端口  不允许
http://www.a.com/a.js
https://www.a.com/b.js 同一域名,不同协议 不允许
http://www.a.com/a.js
http://70.32.92.74/b.js 域名和域名对应ip 不允许
http://www.a.com/a.js
http://script.a.com/b.js 主域相同,子域不同 不允许
http://www.a.com/a.js
http://a.com/b.js 同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js 不同域名 不允许
  • 同源政策的目的,主要是为了保证用户信息的安全,防止恶意的网站窃取数据。设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
  • 同源政策的限制范围: Cookie、LocalStorage 和 IndexDB 无法读取、 DOM 无法获得、 AJAX 请求不能发送。但也有一些标签不受同源策略的标签。
<script src="..."></script>标签嵌入跨域脚本,jsonp的原理主要就是用script标签规避。
<link rel="stylesheet" href="...">标签嵌入CSS,同时css里面background-img、border-img属性也是不受限制的
<img>嵌入图片。
<video> 和 <audio>嵌入多媒体资源
<object>, <embed> 和 <applet>的插件
<frame>和<iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交复制代码

2.通过document.domain进行跨域

-js通过安全Javascript出于对安全性的考虑,而禁止两个或者多个不同域的页面进行互相操作。不同的框架可以 用window获取对象,但却无法获取对应的属性和方法。这时可以通过document.domain进行跨域,但这两个域名 必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域,该方法适 用与cookie和iframe。例如有个拥有iframe的html界面的地址是http://a.example.cn/a.html,另一个界 面为http://b.example.cn/b.html,这两个界面之间是无法通信的,但我们在两个界面分别设置 document.domain='example.com'可以进行通信。

  • 在页面http://a.example.cn/a.html 中设置document.domain:
<!DOCTYPE html>

<head>
  <meta charset="utf-8">
  <title>a.html</title>
  <meta name="description" content="">
</head>
<body>
  <script type="text/javascript">
    document.domain = 'example.com';
    var ifr = document.createElement('iframe');
    ifr.src = 'http://www.script.a.com/b.html';
    ifr.display = none; document.body.appendChild(ifr);
    ifr.onload = function () { 
      var doc = ifr.contentDocument || ifr.contentWindow.document;  //在这里操作doc,也就是b.html          
      ifr.onload = null; 
    }
  </script>

</body>

</html>
  • 在页面http://b.example.cn/a.html 中设置document.domain:
<script >    document.domain = 'example.com';  </script>

cookie只有同源的网页才能共享,但是也可以用document.domain进行规避。举例A网页是http://a.example.com/a.html,B网页是http://b.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。

  • 在http://a.example.com/a.html的代码:
document.domain = 'example.com';
document.cookie = "test1=hello";//a界面设置cookie
  • 在http://a.example.com/b.html的代码
document.domain = 'example.com';
var allCookie = document.cookie;//b界面设置cookie
  • 最常用的方法是在服务端设置一级域名的cookie,这样二级、三级域名也可以设置cookie
Set-Cookie:key=value;domain='example.com';path=/

3.通过window.name跨域

window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面 都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个 窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。window.name始终是string类型。

  • 在页面a.html的相关代码
<!DOCTYPE html>

<head>
  <meta charset="utf-8">
  <title>a.html</title>
  <meta name="description" content="">
</head>

<body>
  <script type="text/javascript">
    iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    var state = 0;
    iframe.onload = function () {
      if (state === 1) {
        var data = JSON.parse(iframe.contentWindow.name);
        //销毁iframe,释放内存。         
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
      }
      else if (state === 0) {
        state = 1;
        //about:blank 也替换成某个同源页面(about:blank,javascript: 和 data: 中的内容,继承了载入他们的页面的源。)         
        iframe.contentWindow.location = ':about:blank';
      }
    }; 
    iframe.src = 'http://b.example.com'; 
    document.body.appendChild(iframe);
  </script>

</body>

</html>
  • 在页面b.html的相关代码
<!DOCTYPE html>

<head>
  <meta charset="utf-8">
  <title>b.html</title>
  <meta name="description" content="">
</head>

<body>
  <script type="text/javascript">
    window.name='aaaaaaaaa'
  </script>

</body>

</html>

这里主要运用的思想是在a.html中加载完iframe将b.html后设置windows.name,然后立即将ifram的将src重置成同源的src,这时候就可以在a.html获取到b.html在iframe中设置的windows.name,与document.name方法相比,放宽了域名后缀要相同的限制,可以从任意页面获取 string 类型的数据

4. 通过location.hash跨域

原理是利用location.hash来进行传值,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递。假设域名a.com下的文件cs1.html要和b.com域名下的cs2.html传递信息, cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向example.com域名下的cs2.html页面, 这时的hash值可以做参数传递用。cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个 页面不在同一个域下IE、Chrome(Firefox可以修改)不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代 理cs3.html对a.html的hash值进行修改。同时在cs1.html上加一个定时器进行轮询,隔一段时间来判断location.hash的值有 没有变化,有变化则获取获取hash值,这样就实现类两个界面之间的通信

  • 先是a.com下的文件cs1.html文件:
<!DOCTYPE html>

<head>
  <meta charset="utf-8">
  <title>cs1.html</title>
  <meta name="description" content="">
</head>

<body>
  <script type="text/javascript">
    function startRequest() { 
      var ifr = document.createElement('iframe');
       ifr.style.display = 'none'; 
       ifr.src = 'http://b.com/cs1.html#data'; 
       document.body.appendChild(ifr); 
    } 
    function checkHash() { 
         try { 
           var data = location.hash ? location.hash.substring(1) : ''; 
           if (console.log) { 
             console.log('Now the data is ' + data); 
             } } 
          catch (e) { 

          }; 
    }
    startRequest(); 
    setInterval(checkHash, 2000);
  </script>

</body>

</html>
  • 在b.com下面cs2.html的相关代码
<!DOCTYPE html>

<head>
  <meta charset="utf-8">
  <title>cs2.html</title>
  <meta name="description" content="">
</head>

<body>
  <script type="text/javascript">
    if (location.hash === '#aaa') {
      try { 
        parent.location.hash = 'otherData';
       } catch (e) {        
           // ie、chrome的安全机制无法修改parent.location.hash,      
           // 所以要利用一个中间的a.com域下的cs3.html代理修改cs.1的hash值       
        var ifrproxy = document.createElement('iframe'); 
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://a.com/cs3.html#otherData';// 注意该文件在"a.com"域下       
        document.body.appendChild(ifrproxy);
      }
    } 
  </script>

</body>

</html>
  • 在a.com下面的cs3.html
    parent.parent.location.hash = location.hash.substring(1)

5. 通过Jsonp进行跨域

script标签引入静态文件不受同源限制的,Jsonp进行跨域的原理主要是利用script标签映入一个文件,然后执行对应的回调函数。但此种方式要跟后端约定好相关的返回数据格式。

  • 原生实现方式
<head>
  <meta charset="utf-8">
  <meta name="description" content="">
</head>
<script src='http://example.cn?callback=callback'></script>
<script type="text/javascript">
  function callback(data){
    console.log(data)
  }
</script>
<body>
</body>
</html>
  • 通过jquery进行请求,但只能发送get请求
$.ajax({
    url:'http://example.cn',
    type:'GET',
    dataType:'jsonp',//请求方式为jsonp
    jsonpCallback:'callback'
})

6.通过HTML5的postMessage方法跨域

postMessage是h5提出来的api,Internet Explorer 8+, chrome,Firefox , Opera 和 Safari 都将支持这个功能。主要包括接受messgae的和发送message的事件。

otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow是指要对那个窗口发送消息,需是iframe和执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
  • message 将要发送到其他 window的数据,类型为 String、Object (IE8、9 不支持)
  • targetOrigin: 是限定消息接收范围,不限制请使用‘*’代替
/*
 * A窗口的域名是<http://example.com:8080>,以下是A窗口的script标签下的代码:
 */

var popup = window.open(...popup details...);

// 如果弹出框没有被阻止且加载完成

// 这行语句没有发送信息出去,即使假设当前页面没有改变location(因为targetOrigin设置不对)
popup.postMessage("The user is 'bob' and the password is 'secret'",
                  "https://secure.example.net");

// 假设当前页面没有改变location,这条语句会成功添加message到发送队列中去(targetOrigin设置对了)
popup.postMessage("hello there!", "http://example.org");

function receiveMessage(event)
{
  // 我们能相信信息的发送者吗?  (也许这个发送者和我们最初打开的不是同一个页面).
  if (event.origin !== "http://example.org")
    return;

  // event.source 是我们通过window.open打开的弹出页面 popup
  // event.data 是 popup发送给当前页面的消息 "hi there yourself!  the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
/*
 * 弹出页 popup 域名是<http://example.org>,以下是script标签中的代码:
 */

//当A页面postMessage被调用后,这个function被addEventListenner调用
function receiveMessage(event)
{
  // 我们能信任信息来源吗?
  if (event.origin !== "http://example.com:8080")
    return;

  // event.source 就当前弹出页的来源页面
  // event.data 是 "hello there!"

  // 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把event.source
  // 作为回信的对象,并且把event.origin作为targetOrigin
  event.source.postMessage("hi there yourself!  the secret response " +
                           "is: rheeeeet!",
                           event.origin);
}

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

任何窗口可以在任何其他窗口访问此方法,在任何时间,无论文档在窗口中的位置,向其发送消息。 因此,用于接收消息的任何事件监听器必须首先使用origin和source属性来检查消息的发送者的身份。 这不能低估:无法检查origin和source属性会导致跨站点脚本攻击。

7.通过CORS跨域

CORS的简介

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

  • 两种请求:简单请求和非简单请求,只要满足下面的两个条件就为简单请求,其余的都为复杂请求,浏览器对这两种请求的处理,是不一样的。
(1)请求方法为GET、POST、PUT
(2)Http请求头不超出Accept、Accept-Language、Content-Language、Last-Even-Id、
以及Content-type不超过multipart/form-data、text/plain、application/x-www-form-urlencode

简单请求的基本流程

浏览器发送请求是简单请求会自动在头信息之中,添加一个Origin字段,Origin(协议+域名+端口),服务器会根据origin判读是否同意这次请求。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

如果Origin不在服务器指定的范围内,服务器会返回一个正常的响应,浏览器发现返回的响应中没有Access-Control-Allow-Origin字段,就知道出错了,从而抛出错误,XmlHttpRequest的onerror回调函数可以捕捉到,这种错误无法被状态码捕捉到,因为状态码返回的是200.如果在指定范围内会返回以下几个字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: content-disposition
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin字段是必须的,指可以是origin的指,也可以是‘*’,表示任意域名的请求

  • Access-Control-Allow-Credentials该字段可选,是个布尔值,表示是否允许发送cookie。默认情况下CORS请求不包括cookie

  • Access-Control-Expose-Headers,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('content-disposition')可以返回content-disposition字段的值。我们在项目中有时会用到下载文件,后台返回的是文件流,要获取到对应的文件名,后台通常会在返回的请求头中添加content-disposition用来表示文件名称,这个时候需要Access-Control-Expose-Headers: content-disposition前端才能获取。

  • CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,需要指定Access-Control-Allow-Credentials字段。同时开发者必须在AJAX请求中打开withCredentials属性。

xhr.withCredentials = false;

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

非简单请求流程

除了上面所说的简单请求的范围,其余的情况都属于非简单请求,非简单请求的CORS请求,会在正式通信之前,会发送一次"预检"请求(preflight),用来判断当前的请求是否合法,如果得到的是肯定的请求,才会在次发送XmlHttpRequest请求,否则报错。

  • 下面是一段浏览器的JavaScript脚本。HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
  • 下面是预请求的http信息,预请求的请求方法是Options
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...

Access-Control-Request-Method该字段是必须的,浏览器会自动添加,列出CORS请求需要用到哪些方法,上面是PUT。

Access-Control-Request-Headers该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。只要你在http请求的headers加上相关的请求头信息。浏览器也会自动添加

  • 预检请求的回应,浏览器收到预检请求后检查origin、Access-Control-Request-Method、Access-Control-Request-Headers,如果符合要求会进行响应,响应信息如下
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

如果否定了请求会返回如下信息

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
  • 服务器回应的其他CORS相关字段如下。
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

Access-Control-Allow-Methods是必须,的它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求

Access-Control-Allow-Headers如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段

Access-Control-Allow-Credentials 表明发送请求的时候可以携带cookie Access-Control-Max-Age 这次预请求的有效时间(1728000秒),在这个有效期内,发送的相同请求不需要预检

浏览器的正常请求和回应

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

  • 面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
  • 下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

参考文档

juejin.cn/post/684490…

juejin.cn/post/684490…

www.ruanyifeng.com/blog/2016/0…

www.ruanyifeng.com/blog/2016/0…

developer.mozilla.org/zh-CN/docs/…