(任性决定以后英文一个月中文一个月交替!:smile:)
何为跨域
Web 站点众多,人心难测。当在访问未知站点时,我们无法确定其是否可信。如果该站点试图读取(譬如通过 ajax)我们在非同源站点如 gmail.com 的私人信息,这不仅侵犯了我们的隐私,同时也可能会损害我们的利益。为了避免此类安全问题的发生,用户代理(如浏览器)通常会遵循同源策略。
源
根据 RFC6454 的定义,源(Origin)由协议(protocol)、主机(host)、端口(port)组成。对于两个 URL,当且仅当三者皆完全匹配时才被视为同源。
对于 http://www.example.com/a/b.html
,同源检测结果如下表:
URL | 同源 | 理由 |
---|---|---|
http://www.example.com/a/c.html | 同 | - |
http://www.example.com/b.html | 同 | - |
https://www.example.com/a/c.html | 不同 | 协议 |
http://example.com/a/c.html | 不同 | 主机 |
http://www.example.com:3000/a/c.html | 不同 | 端口 |
同源策略
对于涉及网络的 API,同源策略(Same-Origin Policy)分别处理发送与接受的请求。通常来说,一个源被允许发送信息至另一个源,但不被允许从另一个源接收信息。它阻止了恶意站点从其他站点读取机密信息,也阻止了网络内容合法地读取其他站点提供的信息。
根据 StackOverflow 上的这个答案,同源策略通常遵循以下规则:
- 规则#1:不允许从不同源读取任何资源
- 规则#2:允许写任意信息至不同源,但规则#1不允许读取响应
- 规则#3:允许自由发送跨域的 GET 与 POST 请求,但无法控制 HTTP 头
对于不同标签、不同 API 同源策略的规则可能会有些许不同。
例如,我之前的一篇答案中引用了维基百科的图片,虽然其能正确的显示,但我无法读取该图像的信息(除了 DOM 元素的属性):
内嵌非同源脚本时,浏览器虽然能正确加载,但在脚本内发生的具体错误是无法显示的,只有同源脚本的错误才能被捕获。如果我试图使用 ajax 请求非同源的脚本文件时,同样地,我们无法获得该脚本的内容。以 Google Analytics 的脚本为例,ajax 无法读取响应:
在 Network 能看见发出的 ajax 请求。浏览器确实下载了文件,但由于同源策略,会阻止我们读取脚本内容:
为何跨域
由于同源策略限制过于严格,但为了实现某些特定功能(或者出于性能考虑),我们需要实现合理的跨域请求。
如何跨域
聪明的开发者想出了很多方法(找到了很多漏洞)来解决这个问题。
document.domain + iframe
在同源策略中,存在例外情况,就是脚本(JavaScript)可以通过 document.domain
方法来改变本身的源。不过,只能设置为当前域的一个后缀(suffix)。例如,对于页面 http://baz.foo.com 来说,其 document.domain
只能设置为 foo.com。如果设置为其他的域,则会报错。在更改域之后,新的域则会作为后续同源检测的依据。注意,对 document.domain
的赋值会导致域的端口号被 null
覆盖,所以如果存在端口号,必须得加上。
利用这一特点,我们可以结合 document.domain 与 <iframe>
实现跨域。由于同源策略,当前页上非同源的 iframe 窗口里的内容我们是无法操作,但如果我们当前页与 iframe 窗口的域都改为相同(合法)域,则当前页与 iframe 页就能相互通信了。这样也就实现了跨域!
一个栗子:
假设父域为 http://foo.com
,子域为 http://baz.foo.com
|
|
JSONP
JSONP (JSON with Padding),是 Web 开发者用来克服浏览器跨域限制的一种 JSON 扩展。如我们所知,AJAX 虽能下载却无法读取非同源的资源,而对于外联的 <script>
标签,浏览器会正确的下载并执行、求值,只是用户无法读取其中内容。借助 <script>
标签的这一特征,我们可以通过 JSONP 拿到跨域数据。
举个栗子。假设存在一个 URL 提供 JSON 类型数据(可能动态从数据库查询),在 HTML 中插入一个链接为此的 <script>
标签并无法获得该处的数据,因为浏览器只把其解析为一个对象,并没有赋值或者别的什么东西。而如果使用 JSONP,<script>
标签 src
属性指向的那个 URL 提供的 JSON 数据必须被一段 JavaScript 代码包裹的数据。这段代码就会被浏览器解析并执行,请求方已定义的函数就能间接地获得 JSON 数据了。
(不过,使用 JSONP 也存在许多安全问题。譬如,在当前页面会注入来自某 URL 的任意内容,这会很危险…)
简易流程的请求与响应如下:
|
|
可以在控制台跑的栗子:
|
|
这是在控制台测试的结果:
window.postMessage
window.postMessage
是 HTML5 引入的可控的安全的可跨域通信的 API。其语法如下:
|
|
- otherWindow:指的是被请求页面的 window 的引用,而不是当前页面
- message:指的是要传输的数据,可以是任意类型
- targetOrigin:指定了被请求页面的源应该满足的格式,可以是 URI(协议、主机、端口三者匹配),或者是 “*”,表示不加限制
多说不如栗子:
假设 A 站(http://acfun.com
)要向 B 站(http://bilibili.com
)发送一个消息,用 postMessage 该如何处理呢?
|
|
CORS
前面提到的三种跨域手段,除了 postMessage,其它的看起来都有点黑魔法:sparkle:的感觉。如果能直接发请求,而不用整这些乱七八糟的东西就再好不过了。由于需求确实存在,W3C Web 工作组提出了一份新的建议标准,跨源资源共享(Cross-Origin Resource Share)。该机制能使得安全地进行跨域数据传输成为可能。该机制需要服务端的配合才可生效。
CORS 针对不同的请求有不同的处理方式,分为简单请求与非简单请求。
简单请求指的是满足以下三点的请求:
- 方法为 GET / HEAD / POST
- 除用户代理设置的头外,只设置了 Accept / Accept-Language / Content-Language / Content-Type
- Content-Type 是以下三种之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
简单请求
浏览器会根据 URL 自动识别 XMLHttpRequest 是否跨域,如果跨域会自动为请求头添加 Origin
字段。发送出去后,如果响应头中包含 Access-Control-Allow-Origin
字段并且其值包含 Origin
的值,浏览器则不会拦截该响应。
例如,我们对 http://freegeoip.net/json/
发出一个 XMLHttpRequest 请求,其请求与响应如图:
如果对服务器未设置 Access-Control-Allow-Origin
响应头的发送请求,该请求会被视作跨域请求而被浏览器拦截。
携带 Cookie
浏览器对 XMLHttpRequest 发起的跨域请求默认是不允许发送 Cookies(与验证信息) 的,在上图中也有体现。但如果将 XMLHttpRequest 的 withCredentials
属性置为 true,浏览器就会允许 Cookies(与验证信息)的发送。
依然对 http://freegeoip.net/json/
发出一个 XMLHttpRequest 请求,并设置 withCredentials
为 true,其请求与响应如图:
不过要注意的是,如果服务端能够响应带 Credentials 的请求,则其允许的源必须为请求者,而不能是 “*”。
预请求
对于非简单请求(不满足那三点),浏览器必须发送一个 OPTIONS 请求(预请求)给目的站点,来验证该跨域请求对目的站点来说是否可接受,以免造成安全事故。
|
|
其请求与响应如图:
发现该服务并不支持非简单请求。
下面是支持非简单请求应有的响应:
koa 提供了一个简单的实现方案,koa-cors。
适用场景
document.domain + iframe
- 支持较老浏览器
- 支持不止于 GET
- 只需父、子域之间通信
JSONP
- 支持较老浏览器
- 只支持 GET
- 支持跨完全不同的域
window.postMessage
- 支持 IE8+,IE8/9 只支持在 iframes/frames 间传递消息,不支持弹出窗口;且 message 只能为字符串。:pill:
- 支持不止于 GET
- 支持跨完全不同的域
CORS (推荐)
- 支持 IE8+
- 支持不止于 GET
- 支持自定义 HTTP 头
- 支持发送 Cookies
- 支持跨完全不同的域
参考文献
- RFC 6454 - The Web Origin Concept
- W3C - Same-Origin Policy
- MDN - Same-Origin Policy
- StackOverflow - Why do browser APIs restrict cross-domain requests?
- StackOverflow - Ways to circumvent the same-origin policy
- Wikipedia - JSONP
- MDN - postMessage
- MDN - HTTP Access Control
- 跨域资源共享 CORS 详解
- cross-domain-with-koa