由同源策略到前端跨域

目录
  1. 导读
  2. 同源策略的限制
    1. iframe限制
    2. Ajax限制
    3. Script限制
    4. 注意
  3. 跨域访问
    1. 使用代理
    2. JSONP
    3. postMessage
    4. CORS 跨域访问
    5. document.domain
      1. Internet Explorer同源策略绕过
    6. window.name
    7. location.hash
    8. Access Control
    9. flash URLLoder
    10. WebSocket

导读

同源策略 (Same-Origin Policy) 最早由 Netscape 公司提出, 所谓同源就是要求, 域名, 协议, 端口相同. 非同源的脚本不能访问或者操作其他域的页面对象(如DOM等). 作为著名的安全策略, 虽然它只是一个规范, 并不强制要求, 但现在所有支持 javaScript 的浏览器都会使用这个策略. 以至于该策略成为浏览器最核心最基本的安全功能, 如果缺少了同源策略, web的安全将无从谈起.

同源策略的限制

同源策略下的web世界, 域的壁垒高筑, 从而保证各个网页相互独立, 互相之间不能直接访问, iframe, ajax 均受其限制, 而script标签不受此限制.

注: 以下如非特别说明, 均指非CORS的, 普通跨域请求.

iframe限制

  • 可以访问同域资源, 可读写;
  • 访问跨域页面时, 只读.

Ajax限制

Ajax 的限制比 iframe 限制更严.

  • 同域资源可读写;
  • 跨域请求会直接被浏览器拦截.(chrome下跨域请求不会发起, 其他浏览器一般是可发送跨域请求, 但响应被浏览器拦截)

Script限制

script并无跨域限制, 这是因为script标签引入的文件不能够被客户端的 js 获取到, 不会影响到原页面的安全, 因此script标签引入的文件没必要遵循浏览器的同源策略. 相反, ajax 加载的文件内容可被客户端 js 获取到, 引入的文件内容可能会泄漏或者影响原页面安全, 故, ajax必须遵循同源策略.

注意

同源策略要求三同, 即: 同域, 同协议, 同端口.

  • 同域即host相同, 顶级域名, 一级域名, 二级域名, 三级域名等必须相同, 且域名不能与 ip 对应;
  • 同协议要求, http与https协议必须保持一致;
  • 同端口要求, 端口号必须相同.

IE有些例外, 它仅仅只是验证主机名以及访问协议,而忽略了端口号.

这里需要澄清一个概念, 所谓的域, 跟 js 等资源的存放服务器没有关系, 比如你到 baidu.com 使用 script 标签请求了 google.com 下的js, 那么该 js 所在域是 baidu.com, 而不是 google.com. 换言之, 它能操作baidu.com的页面对象, 却不能操作google.com的页面对象.

跨域访问

实际上, 我们又不可避免地需要做一些跨域的请求, 下面提供几种方案去绕过同源策略:

使用代理

虽然ajax和iframe受同源策略限制, 但服务器端代码请求, 却不受此限制, 我们可以基于此去伪造一个同源请求, 实现跨域的访问. 如下便是实现思路:

  1. 请求同域下的web服务器;
  2. web服务器像代理一样去请求真正的第三方服务器;
  3. 代理拿到数据过后, 直接返回给客户端ajax.

这样, 我们便拿到了跨域数据.

JSONP

由上, script标签并不受同源策略约束, 基于script 标签可做 jsonp 形式的访问, 可以通过第三方服务器生成动态的js代码来回调本地的js方法,而方法中的参数则由第三方服务器在后台获取,并以JSON的形式填充到JS方法当中. 即 JSON with Padding. 具体如下:

1) 可用js生成以下html 代码, 去做jsonp的请求.

<script type="text/javascript" src="https://www.targetDomain.com/jsonp?callback=callbackName"></script>

使用 jquery, 即

jQuery.getJSON(
  "https://www.yourdomain.com/jsonp?callback=?",
  function(data) {
      console.log("name: " + data.name);
  }
);

其中回调函数名 “callback” 为 “?”, 即不需要用户指定,而是由jquery生成.

2) 服务器端,以 java 为例, 参考如下:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  //获取JSON数据
  String jsonData = "{\"name\":\"jsonp\""}";
  //获取回调函数名
  String callback = req.getParameter("callback");  
  //拼接动态JS代码
  String output = callback + "(" + jsonData + ");
    resp.setContentType("text/javascript");
  PrintWriter out = resp.getWriter();
  out.println(output);
  // 响应为 callbackName({\"name\":\"jsonp\""});
}

postMessage

ES5新增的 postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递.

语法: postMessage(data,origin)

data: 要传递的数据,html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候建议使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果.

origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然如果愿意也可以建参数设置为”*”,这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。

父页面发送消息:

window.frames[0].postMessage('message', origin)

iframe接受消息:

window.addEventListener('message',function(e){
    if(e.source!=window.parent) return;//若消息源不是父页面则退出
      //TODO ...
});

其中 e 对象有三个重要的属性

  • data, 表示父页面传递过来的message
  • source, 表示发送消息的窗口对象
  • origin, 表示发送消息窗口的源(协议+主机+端口号)

CORS 跨域访问

HTML5带来了一种新的跨域请求的方式 — CORS, 即 Cross-origin resource sharing. 它更加安全, 上述的 JSONP, postMessage 等, 资源本身没有能力保证自己不被滥用. CORS的目标是保护资源只被可信的访问源以正确的方式访问.

目前, 主流的浏览器都支持此协议, 可以在caniuse.com 中查到http://caniuse.com/#search=cors.

简而言之, 浏览器不再一味禁止跨域访问, 而是检查目的站点的响应头域, 进而判断是否允许当前站点访问. 通常, 服务器使用以下的这些响应头域用来通知浏览器:

Response headers[edit]
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Expose-Headers
Access-Control-Max-Age

CORS的解决办法是在服务端Response的HTTP头域加入资源的访问权限信息. 如: A站只需要在response头中加一个字段就能让B站跨站访问.

access-control-allow-origin:*

其中* 表示通配, 所有的域都能访问此资源, 如果严谨一些只允许B站访问:

access-control-allow-origin:<B-DOMAIN>

这样B站就可以直接访问此资源, 不需要JSONP 也不需要iframe了.

CORS需要指定METHOD访问, 对于GET和POST请求, 至少要指定以下三种methods, 如下:

Access-Control-Allow-Methods: POST, GET, OPTIONS

如果是POST请求, 且提交的数据类型是json, 那么, CORS需要指定headers.

Access-Control-Allow-Headers: Content-Type

CORS默认是不带cookie的, 设置以下字段将允许浏览器发送cookie.

Access-Control-Allow-Credentials: true

除此之外, 为了跨站发送cookie等验证信息, access-control-allow-origin 字段将不允许设置为*, 它需要明确指定与请求网页一致的域名.

同时, 请求网页中需要做如下显式设置才能真正发送cookie.

xhr.withCredentials = true;

document.domain

通过修改document的domain属性,我们可以在域和子域或者不同的子域之间通信(即它们必须在同一个一级域名下). 同域策略认为域和子域隶属于不同的域,比如a.com和 script.a.com是不同的域,这时,我们无法在a.com下的页面中调用script.a.com中定义的JavaScript方法。但是当我们把它们document的domain属性都修改为a.com,浏览器就会认为它们处于同一个域下,那么我们就可以互相获取对方数据或者操作对方DOM了。

比如, 我们在 www.a.com/a.html 下, 现在想获取 www.script.a.com/b.html, 即主域名相同, 二级域名不同. 那么可以这么做:

document.domain = 'a.com';
var iframe = document.createElement('iframe');
iframe.src = 'http://www.script.a.com/b.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.addEventListener('load',function(){
    //TODO 载入完成时做的事情
    //var _document = iframe.contentWindow.document;
     //...
},false);

注意:

  • 2个页面都要设置, 哪怕 a.html 页已处于 a.com 域名下, 也必须显式设置.
  • document.domain只能设置为一级域名,比如这里a页不能设置为www.a.com (二级域名).

利用domain属性跨域具有以下局限性:

  • 两个页面要在同一个一级域名下, 且必须同协议, 同端口, 即子域互跨;
  • 只适用于iframe.
Internet Explorer同源策略绕过

Internet Explorer8以及前面的版本很容易通过document.domain实现同源策略绕过, 通过重写文档对象, 域属性这个问题可以十分轻松的被利用.

var document;
document = {};
document.domain = 'http://www.a.com';
console.log(document.domain);

如果你在最新的浏览器中运行这段代码, 可能在JavaScript控制台会显示一个同源策略绕过错误.

window.name

window 对象的name属性是一个很特别的属性, 当该window的location变化, 然后重新加载, 它的name属性可以依然保持不变. 那么我们可以在页面 A中用iframe加载其他域的页面B, 而页面B中用JavaScript把需要传递的数据赋值给window.name, iframe加载完成之后(iframe.onload), 页面A修改iframe的地址, 将其变成同域的一个地址, 然后就可以读出iframe的window.name的值了(因为A中的window.name和iframe中的window.name互相独立的, 所以不能直接在A中获取window.name, 而要通过iframe获取其window.name). 这个方式非常适合单向的数据请求,而且协议简单、安全. 不会像JSONP那样不做限制地执行外部脚本.

location.hash

location.hash(两个iframe之间), 又称FIM, Fragment Identitier Messaging的简写.

因为父窗口可以对iframe进行URL读写, iframe也可以读写父窗口的URL, URL有一部分被称为hash, 就是#号及其后面的字符, 它一般用于浏览器锚点定位, Server端并不关心这部分, 所以这部分的修改不会产生HTTP请求, 但是会产生浏览器历史记录. 此方法的原理就是改变URL的hash部分来进行双向通信. 每个window通过改变其他 window的location来发送消息(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe), 并通过监听自己的URL的变化来接收消息. 这个方式的通信会造成一些不必要的浏览器历史记录, 而且有些浏览器不支持onhashchange事件, 需要轮询来获知URL的改变, 最后, 这样做也存在缺点, 比如数据直接暴露在了url中, 数据容量和类型都有限等.

Access Control

此跨域方法目前只在很少的浏览器中得以支持, 这些浏览器可以发送一个跨域的HTTP请求(Firefox, Google Chrome等通过XMLHTTPRequest实现, IE8下通过XDomainRequest实现), 请求的响应必须包含一个Access- Control-Allow-Origin的HTTP响应头, 该响应头声明了请求域的可访问权限. 例如baidu.com对google.com下的getUsers.php发送了一个跨域的HTTP请求(通过ajax), 那么getUsers.php必须加入如下的响应头:

header("Access-Control-Allow-Origin: http://www.baidu.com");//表示允许baidu.com跨域请求本文件

flash URLLoder

flash有自己的一套安全策略, 服务器可以通过crossdomain.xml文件来声明能被哪些域的SWF文件访问, SWF也可以通过API来确定自身能被哪些域的SWF加载. 当跨域访问资源时, 例如从域 a.com 请求域 b.com上的数据, 我们可以借助flash来发送HTTP请求.

  • 首先, 修改域 b.com上的 crossdomain.xml(一般存放在根目录, 如果没有需要手动创建) , 把 a.com 加入到白名单;
<?xml version="1.0"?>
<cross-domain-policy>
<site-control permitted-cross-domain-policies="by-content-type"/>
<allow-access-from domain="a.com" />
</cross-domain-policy>
  • 其次, 通过Flash URLLoader发送HTTP请求, 拿到请求后并返回;
  • 最后, 通过Flash API把响应结果传递给JavaScript.

Flash URLLoader是一种很普遍的跨域解决方案,不过需要支持iOS的话,这个方案就不可行了.

WebSocket

在WebSocket出现之前, 很多网站为了实现实时推送技术, 通常采用的方案是轮询(Polling)和Comet技术, Comet又可细分为两种实现方式, 一种是长轮询机制, 一种称为流技术, 这两种方式实际上是对轮询技术的改进, 这些方案带来很明显的缺点, 需要由浏览器对服务器发出HTTP request, 大量消耗服务器带宽和资源. 面对这种状况, HTML5定义了WebSocket协议, 能更好的节省服务器资源和带宽并实现真正意义上的实时推送.

WebSocket 本质上是一个基于TCP的协议, 它的目标是在一个单独的持久链接上提供全双工(full-duplex), 双向通信, 以基于事件的方式, 赋予浏览器实时通信能力. 既然是双向通信, 就意味着服务器端和客户端可以同时发送并响应请求, 而不再像HTTP的请求和响应. (同源策略对 web sockets 不适用)

原理: 为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求, 这个请求和通常的HTTP请求不同, 包含了一些附加头信息, 其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求, 服务器端解析这些附加的头信息然后产生应答信息返回给客户端, 客户端和服务器端的WebSocket连接就建立起来了, 双方就可以通过这个连接通道自由的传递信息, 并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接.

一个典型WebSocket客户端请求头:

WebSocket客户端请求头


本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.

本文作者: louis

本文链接: http://louiszhai.github.io/2016/03/02/cross-domain/

参考文章

Fork me on GitHub