同源限制和跨域笔记

1,317 阅读7分钟

1. 同源限制

1.1 同源含义

同源需要保持三个相同:

协议相同,域名相同,端口相同

举例来说,http://www.example.com/dir/page.html这个网址,协议是http://,域名是www.example.com,端口是80(默认端口可以省略)

1.2 同源的目的

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据,让Cookie 不能共享

1.3 非同源的限制

(1) 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。

(2) 无法接触非同源网页的 DOM。

(3) 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。

2. Cookie

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain共享 Cookie。

举例来说,A 网页的网址是http://w1.example.com/a.html,B 网页的网址是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享 Cookie。因为浏览器通过document.domain属性来检查是否同源。

// 两个网页都需要设置
document.domain = 'example.com';

3. 跨域窗口的通信

  • 片段识别符(fragment identifier)
  • 跨文档通信API(Cross-document messaging)

3.1 片段标识符

片段标识符指的是,URL 的#号后面的部分,比如http://example.com/x.html#fragment#fragment。如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段标识符。

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

上面代码中,父窗口把所要传递的信息,写入 iframe 窗口的片段标识符。

子窗口通过监听hashchange事件得到通知。

window.onhashchange = checkMessage;

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

同样的,子窗口也可以改变父窗口的片段标识符。

parent.location.href = target + '#' + hash;

3.2 跨文档通信API

window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。

// 父窗口打开一个子窗口
var popup = window.open('http://bbb.com', 'title');
// 父窗口向子窗口发消息
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。

父窗口和子窗口都可以通过message事件,监听对方的消息。

// 父窗口和子窗口都可以用下面的代码,
// 监听 message 消息
window.addEventListener('message', function (e) {
  console.log(e.data);
},false);

message事件的参数是事件对象event,提供以下三个属性。

  • event.source:发送消息的窗口
  • event.origin: 消息发向的网址
  • event.data: 消息内容

通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能,也就是使用postMessage发送LocalStorage的内容

4. 跨域

同源政策规定,AJAX 请求只能发给同源的网址,否则就报错。

除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制:JSONP、WebSocket、CORS

4.1 JSONP

浏览器端通过

第一步,网页添加一个<script>元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。

<script src="http://api.foo.com?callback=bar"></script>

注意,请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar)。

第二步,服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}))。

第三步,客户端会将服务器返回的字符串,作为代码解析,因为浏览器认为,这是<script>标签请求的脚本内容。这时,客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据。

示例代码:

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);
};

4.2 WebSocket

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

原因是通过WebSocket进行通信,浏览器发出的 WebSocket 请求的头信息包含一个Origin字段,表示该请求的请求源,服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就能进行回应

4.3 CORS

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。整个 CORS 通信过程,都是浏览器自动完成

浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。

4.3.1 CORS请求分类

简单请求
  1. 请求方法是GET、POST、HEAD
  2. HTTP 的头信息不超出以下几种字段。
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

同时满足这两个条件就是简单请求

对于简单请求,浏览器直接发出CORS请求,具体来说就是在头部信息增加一个Origin字段,说明本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求

简单请求响应,会多出几个头信息字段(如果Origin指定的域名在许可范围内)

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

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,“预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method

该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法

  • Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段

预检请求响应

服务器收到“预检”请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应

如果服务器否定了“预检”请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段,或者明确表示请求不符合条件。

浏览器会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获

“预检”请求之后,浏览器的正常 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...

4.3.2 CORS 请求发送 Cookie 信息

CORS 请求默认不包含 Cookie 信息(以及 HTTP 认证信息等),这是为了降低 CSRF 攻击的风险。

如果服务器需要拿到Cookie,需要服务器显式指定Access-Control-Allow-Credentials字段,告诉浏览器可以发送 Cookie。

Access-Control-Allow-Credentials: true

同时,开发者必须在 AJAX 请求中打开withCredentials属性。

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

并且,Access-Control-Allow-Origin不能设为星号,必须指定明确的、与请求网页一致的域名,Cookie 依然遵循同源政策