Skip to content

浏览器同源策略

浏览器同源策略 是什么?

  • Same-origin policy 它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同" 协议相同、域名相同、端口相同

浏览器同源策略 解决了什么问题?

  • 为了保证用户信息的安全,防止恶意的网站窃取数据,是浏览器安全的基石

  • 设想这样一种情况: A 网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取 A 网站的 Cookie,会发生什么?

  • 很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了

规避方式:iframe

  • 如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的 document.domain 属性,就可以规避同源政策;对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题

片段标识符(fragment identifier)

javascript
// 父窗口可以把信息,写入子窗口的片段标识符
var src = originURL + "#" + data;
document.getElementById("myIFrame").src = src;

// 子窗口通过监听hashchange事件得到通知。
window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

// 子窗口也可以改变父窗口的片段标识符
parent.location.href = target + "#" + hash;

window.name

  • 浏览器窗口有 window.name 属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它
  • 优点:window.name 容量很大,可以放置非常长的字符串;
  • 缺点:必须监听子窗口 window.name 属性的变化,影响网页性能。

window.postMessage

  • HTML5 为了解决这个问题,引入了一个全新的 API:跨文档通信 API;这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源
javascript
// message事件的事件对象event,提供以下三个属性
// event.source:发送消息的窗口
// event.origin: 消息发向的网址
// event.data: 消息内容

// 子窗口通过event.source属性引用父窗口,然后发送消息
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
  event.source.postMessage("Nice to see you!", "*");
}

LocalStorage

  • window.postMessage,读写其他窗口的 LocalStorage 也成为了可能
javascript
// 父窗口发送消息
var win = document.getElementsByTagName("iframe")[0].contentWindow;
var obj = { name: "Jack" };
win.postMessage(
  JSON.stringify({ key: "storage", data: obj }),
  "http://bbb.com"
);

// 子窗口将父窗口发来的消息,写入自己的LocalStorage
window.onmessage = function (e) {
  if (e.origin !== "http://bbb.com") {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

规避方式:Ajax 之 Jsonp

  • 基本思想是,通过添加一个<script>元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来
javascript
function addScriptTag(src) {
  var script = document.createElement("script");
  script.setAttribute("type", "text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag("http://example.com/ip?callback=foo");
};

function foo(data) {
  console.log("Your public IP address is: " + data.ip);
}

规避方式:Ajax 之 WebSocket

  • WebSocket 是一种通信协议,使用 ws://(非加密)和 wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信

浏览器发出的 WebSocket 请求的头信息

http

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
  • 有一个字段是 Origin,表示该请求的请求源(origin),即发自哪个域名。正是因为有了 Origin 这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信

规避方式:Ajax 之 CORS

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

简单请求 和 非简单请求

  • 浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request);浏览器对这两种请求的处理,是不一样的
  • 只要同时满足以下两大条件,就属于简单请求
  1. 请求方法是以下三种方法之一:

    请求方法
    HEAD
    GET
    POST
  2. HTTP 的头信息不超出以下几种字段

HTTP 的头信息不超出以下几种字段允许的头信息字段及其内容限制
Accept任意值,表示客户端可接受的内容类型范围
Accept-Language任意值,表示客户端优先的语言区域设置
Content-Language任意值,表示请求体中的内容所使用的语言
Last-Event-ID任意值,用于服务器向客户端发送事件流时识别事件序列
Content-Type只限于以下三个值:
1. application/x-www-form-urlencoded
2. multipart/form-data
3. text/plain

注:以上表格中,“任意值”表示这些字段可以携带用户自定义的内容,但必须在 CORS 跨域资源共享策略允许范围内。

简单请求 基本流程

  • 对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段

  • Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求

  • 如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获

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

  • 如果 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段

HTTP Header(响应头)描述
Access-Control-Allow-Origin必须的。它的值要么是请求时 Origin 字段的值,要么是一个\*,表示接受任意域名的请求求。
Access-Control-Allow-Credentials一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true,如果服务器不要浏览器发送 Cookie,删除该字段即可
Access-Control-Expose-Headers请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。
http
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

特别注意:withCredentials 属性

  • CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials 字段;
  • 另一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性。
  • 否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理
  • 但是,如果省略withCredentials设置,有的浏览器还是会一起发送 Cookie。这时,可以显式关闭 withCredentials
  • 需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名
javascript
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

非简单请求 流程

  • 非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)

  • 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 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 头信息

http
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...
  • 预检"请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源
    • Access-Control-Request-Method:字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法
    • Access-Control-Request-Headers:字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header

预检请求的回应

  • 服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应
http
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
  • 上面的 HTTP 回应中,关键的是 Access-Control-Allow-Origin 字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求

  • 如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest 对象的 onerror 回调函数捕获

javascript
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:字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求
    • Access-Control-Allow-Headers:如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
    • Access-Control-Allow-Credentials:该字段与简单请求时的含义相同
    • Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另一条预检请求。
http
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

浏览器的正常请求和回应

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

总结

解决方案

解决方案描述
Cookie通过在请求中携带 Cookie 信息进行跨域通信。
document.domain适用于主域相同,子域不同的情况,通过设置 document.domain 实现跨域通信。
iframe 片段识别符通过在 URL 的片段标识符中传递信息进行跨域通信。
window.name利用 window.name 属性进行跨域通信。
window.postMessage使用 HTML5 中新增的 window.postMessage 方法进行跨域通信。
LocalStorage利用浏览器本地存储进行跨域通信。
JSONP通过动态生成 script 标签,利用 src 属性加载跨域资源,实现跨域通信。
WebSocket使用 WebSocket 协议进行跨域通信,不受同源策略限制。
CORS在服务器端设置响应头,允许指定的域访问资源,从而实现跨域请求。

CORS 关键响应头

请求头

请求头字段描述
Origin用来说明本次请求来自哪个源(协议 + 域名 + 端口)。
Access-Control-Request-Method用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。
Access-Control-Request-Headers指定浏览器 CORS 请求会额外发送的头信息字段。

响应头

响应头字段描述
Access-Control-Allow-Origin指定允许访问资源的域名
Access-Control-Allow-Methods指定服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers指定服务器支持的所有头信息字段
Access-Control-Allow-Credentials指定是否允许发送 Cookie。
Access-Control-Max-Age指定本次预检请求的有效期,
Access-Control-Expose-Headers指定浏览器可以访问的响应头字段

参考文献

Released under the MIT License.