在前端开发的过程中,我们经常遇到"跨域"的问题,以下的文章将列举一下我在工作中碰到的跨域问题。
以及稍稍的探讨一下为什么会有"跨域"问题的出现,和所谓的"同源策略"
同源策略
1. 历史
1995 年由 Netscape
公司提出,之后被其他浏览器厂商采纳。
同源策略只是一个规范,并没有指定其具体的使用范围和实现方式,各个浏览器厂商都针对同源策略做了自己的实现。
一些 web 技术都默认采取了同源策略,这些技术范围包括但不限于Silverlight
, Adobe Flash
, Adobe Acrobat
, Dom
, XMLHttpRequest
。
2. 定义
Under the policy, a web browser permits scripts contained in a first web page to access data in a second web page, but only if both web pages have the same origin. An origin is defined as a combination of URI scheme, hostname, and port number.
判断同源的三个要素:
- 相同的协议
- 相同的域名
- 相同的端口号
3. 存在的意义
为了保证使用者信息的安全,防止恶意网站篡改用户数据
举个例子:
假设没有同源策略,那么我在A网站下的cookie
就可以被任何一个网站拿到;那么这个网站的所有者,就可以使用我的cookie
(也就是我的身份)在A网站下进行操作。
同源策略可以算是 web 前端安全的基石,如果缺少同源策略,浏览器也就没有了安全性可言。
4. 限制范围
非同源的网站之间
- 无法共享 cookie, localStorage, indexDB
- 无法操作彼此的 dom 元素
- 无法发送 ajax 请求
- 无法通过 flash 发送 http 请求
- 其他
跨域
同源策略做了很严格的限制,但是在实际的场景中,又确实有很多地方需要突破同源策略的限制,也就是我们常说的跨域
1. cookie
同源策略最早被提出的时候,为的就是防止不同域名的网页之间共享 cookie,但是如果两个网页的一级域名是相同的,可以通过设置 document.domain
来共享 cookie。
举个例子,https://market.douban.com
和https://book.douban.com
,这两个网页的一级域名都是 douban.com
,如果我在 market.douban.com
中执行了
document.domain = 'douban.com'
document.cookie = 'cross=yes'
或
document.cookie = 'cross=yes;path=/;domain=douban.com'
这样设置了 cookie 之后,在 book.douban.com
中是可以取到这个 cookie 的。
除了在前端设置之外,也可以直接在 response 里将 cookie 的 domain 设置成 .douban.com
。
2. Ajax
在使用 ajax 的过程中,我们碰到的同源限制的问题是最多的。
针对 ajax ,我们有三种方式可以绕过同源策略的限制:
2.1 设置 CORS
设置 cross-domain 是目前在 ajax 中最常用的一种跨域的方式,相比jsonp
和websoket
也是最安全的一种方式。
唯一美中不足的是低版本的浏览器支持的不是很好
IE ✘ 5.5+ ◒ 8+² ◒ 10+¹ ✔ 11Edge ✔
Firefox ✘ 2+ ✔ 3.5+
Chrome ◒ 4+¹ ✔ 13+
Safari ✘ 3.1+ ◒ 4+¹ ✔ 6+³
Opera ✘ 9+ ✔ 12+
¹Does not support CORS for images in
<canvas>
²Supported somewhat in IE8 and IE9 using the XDomainRequest object (but has limitations)
³Does not support CORS for
<video>
in<canvas>
: https://bugs.webkit.org/show_...
2.1.1 CORS 的运作
CROS 的设置,大部分是需要在服务端进行设置,在服务端设置之前,先来看一下 CROS 在浏览器中是怎么运作的:
首先,在浏览器中,http 请求将被分为两种 简单请求(simple request)
和 非简单请求(not-so-simple request)
。
简单请求的判断包括两个条件:
-
请求方法必须是一下几种:
- HEAD
- GET
- POST
-
HTTP 头只能包括以下信息:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type: 只限于[application/x-www-form-urlencoded, multipart/form-data, text/plain]
不能同时满足以上两个条件的,就都视作非简单请求
2.1.2 简单请求(simple request)
浏览器端
浏览器在处理简单请求时,会在 Header 中加上一个 origin(protocal + host + path + port)
字段,来标明这个请求是来自哪里。
在 CROS 请求中,默认是不会携带 cookie
之类的用户信息的,但是不携带用户信息的话,是没办法判断用户身份的,所以,可以在请求时将withCredentials
设置为 true, 例如:
var xhr = new XMLHttpRequest()
xhr.withCredentials = true
设置了这个值之后,在服务端会将 response
中的 Access-Control-Allow-Credentials
也设置为 true
,这样浏览器才会相应 cookie
服务端
在服务端拿到这个请求之后,会对 origin 进行判断,如果是在允许范围内的请求,将会在 respones 返回的 Header 中加上:
Access-Control-Allow-Origin: origin
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: something
下面来说说这几个字段都代表什么:
- Access-Control-Allow-Origin
看名字大概就能猜出来,这个就是告诉浏览器,服务端接受那些域名的访问。值可以是
request
中的origin
,也可以是*
,也可以是originA | originB
这样的形式,但是目前看来,在浏览器中只支持单一值和*
两种方式。具体可以参考这里:access-control-allow-origin-response-header - Access-Control-Allow-Credentials
从名字上来看,这个字段标明了是否拥有用户相关的权限。
在浏览器中,具体表现为是否可以发送 cookie。这个值可以选择性返回,如果不返回的话,默认就 是不允许发送 cookie,如果返回,则只能返回 true。
另外,如果这个值被设为了
true
,那么Access-Control-Allow-Origin
就不能被设置为*
,必须要显示指定为origin
的值;并且返回的cookie
因为是在被跨域访问的域名下,因为遵守同 源策略,所以在origin
网页中是不能被读取到的。 -
Access-Control-Expose-Headers
从字面意义上来看,这个字段返回的就是其他可被返回的数据。
之所以会有这个字段,是因为在
简单请求
中,response
返回的头信息中,浏览器只能拿到以下几个基本字段:Cache-Control
,Content-Language
,Content-Type
,Expires
,Last-Modified
,Pragma
。如果想要拿到更多的额外信息,只能在
Access-Control-Expose-Headers
里设置,例如:Access-Control-Expose-Headers: "Foo=foo"
这样的话,在浏览中,就可以获取
Foo
这个字段所携带的信息了
2.1.3 非简单请求(not-so-simple request)
与简单请求
最大的不同在于,非简单请求
实际上是发送了两个请求。
预请求
首先,在正式请求之前,会先发送一个预请求(preflight-request)
,这个请求的作用是尽可能少的携带信息,供服务端判断是否响应该请求。
浏览器
浏览器发送预请求
,请求的 Request Method
会设置为 options
。
另外,还会带上这几个字段:
- Origin: 同
简单请求
的origin
- Access-Control-Request-Method: 请求将要使用的方法
- Access-Control-Request-Headers: 浏览器会额外发送哪些头信息
服务端
服务端收到预请求
之后会根据request
中的origin
,Access-Control-Request-Method
和Access-Control-Request-Headers
判断是否响应该请求。
如果判断响应这个请求,返回的response
中将会携带:
- Access-Control-Allow-Origin: origin
- Access-Control-Allow-Methods: like request
- Access-Control-Allow-Headers: like request
如果否定这个请求,直接返回不带这三个字段的response
就可以,浏览器将会把这种返回判断为失败的返回,触发onerror
方法
正式响应
如果预请求
被正确响应,接下来就会发送正式请求,正式请求的request
和正常的 ajax 请求基本没有区别,只是会携带 origin
字段;response
和简单请求
一样,会携带上Access-Control-*
这些字段
2.2 websocket
websocket 不遵循同源策略。
但是在 websocket 请求头中会带上 origin
这个字段,服务端可以通过这个字段来判断是否需要响应,在浏览器端并没有做任何限制。
2.3 jsonp
jsonp 其实算是一种 hack 形式的请求。
jsonp 的本质其实是请求一段 js 代码,是对静态文件资源的请求,所以并不遵循同源策略。但是因为是对静态文件资源的请求,所以只能支持 GET
请求,对于其他方法没有办法支持。
3. iframe
3.1 iframe 中的同源策略
根据同源策略的规定,如果两个页面不同源,那么相互之间其实是隔离的。
在使用 iframe 的页面中,虽然我们可以通过iframe.contentWindow
,window.parent
,window.top
等方法拿到window
对象,但是根据同源策略,浏览器将对非同源的页面之间的window
和location
对象添加限制
不同源的两个网页将不能:
- 操作彼此的 dom
- 获取/调用彼此
window
对象中的属性/方法
不同源的两个网页可以:
- 改变父/子级的 url
具体的规则可以参考这里:integration-with-idl
但是在现实世界中,有很多场景下,其实是需要两个非同源的 iframe 之间进行“跨域”操作的。为了实现这种“跨域”,我们借用了以下几种方法:
- 片段标识符(fragment identifier)
- 使用 window.name
- 跨文档通信
3.2 使用片段标识符(fragment identifier)
片段标识符
指的就是 url 中 #
之后的部分,也就是我们常说的 location.hash
。
使用片段标识符依托于以下几个关键点:
- 改变 url 里的这个部分,是不会触发页面的刷新的
- 父级页面虽然不能操作 iframe 中的
window
和dom
,但是可以改变 iframe 的url
- window 对象可以监听
hashchange
事件
通过这几个关键点,可以实现基于 hashchange
来操作页面
3.3 使用 window.name
window.name
这个属性最厉害的地方在于,window
对象没有改变的话,这个 window
跳转的网页,都读取 window.name
这个值。
例如,A 网页设置了 window.name
,然后跳转到了 B 网页,但是 B 网页中,仍然可以读取到 A 设置的 window.name
通过这个特性,在 iframe 中,子页面可以先设置 window.name
;
然后跳转到一个跟父页面同级的地址,这个 window.name
依然存在,因为已经调到了跟父级页面同源的地址中,所以父页面可以获取到 iframe.contentWindow
中属性,也就是可以读取到 window.name
了
这种方法最大的优点就是window.name
可以传一个很长的字符串,但是缺点也比较明显,就是需要在父级页面不停的去检查子页面的window.name
是否被改变
3.4 跨文档通信API(Cross-document messaging)
虽然上面的两种方法都可以实现不同源页面之间的通信,但是总归是属于hack
的方法,眼看着大家对非同源页面的通信都有需求,所以在 HTML5 规范中,添加了一个window.postMessage
的方法。
通过这个方法,可以方便的实现不同源的页面之间的通信。
看一个简单的例子:
// Page Foo
iframe.contentWindow.postMessage('Hello from foo', '/path/to/bar')
// Page Bar
window.parent.addEventListener('message', function (e) {
console.log(e.source) // 发送消息的窗口
console.log(e.origin) // 消息发向的网址
console.log(e.data) // 消息内容
})
2.6 canvas
在 canvas
的使用过程中,也会碰到同源策略的限制。
以下的几种操作,都会受到同源策略的限制:
- canvas.toDataURL
- canvas.toBlob
- canvas.getContent('2d').getImageData(x,y,w,h)
例如:
// 这段 JS 运行在 a.com 这个域名下
var canvas = document.createElement('canvas')
var ctx = canvas.getContent('2d')
var src = 'http://b.com/path/to/a/image'
var img = new Image()
img.onload = function () {
canvas.with = img.style.width
canvas.height = img.style.height
ctx.drawImage(img)
// 以下的这这三种操作都会报错
canvas.toDataURL('image/jpg')
canvas.toBlob(function () {})
ctx.getImageData(0, 0, 10, 10)
}
img.src = src
运行时会报错
Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
可以看到是toDataURL
的时候,因为 a.com
和b.com
是不同源的两个网页,触发了同源策略的限制。换成toBlob
或getImageData
会报同样的错误。
我们来探究以下报这个错误的原因:
首先,所有bitmaps
类型的对象,在被canvas
或ImageBitmap
使用时,都会先检查当前这对象,是不是处在origin clean
的状态。
然后,所有bitmaps
类型的对象,默认情况下,这个origin clean
都是true
,但是如果这个bitmaps
被跨域调用,那么,这个origin clean
将会被设置成 false
。
再然后,在使用toDataURL
,toBlob
和getImageData
时,都会先检查origin clean
,如果为 false
的话,就会抛出SecurityError
这样的异常。
那么,这个origin clean
的状态,是如何设置的呢?
可以通过crossOrigin
来设置,看代码:
var canvas = document.createElement('canvas')
var ctx = canvas.getContent('2d')
var src = 'http://b.com/path/to/a/image'
var img = new Image()
img.onload = function () {
canvas.with = img.style.width
canvas.height = img.style.height
ctx.drawImage(img)
canvas.toDataURL('image/jpg')
}
img.crossOrigin = '*'
img.src = src
加上了crossOrigin
这个属性,然后执行,发现还会报个错:
Image from origin 'http://b.com' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access
看报错信息大概可以知道,是Access-Control-Allow-Origin
这里出了问题,只需要把Access-Control-Allow-Origin
设置成对应的值就可以了。
更具体的原因可以参考这里:Security with canvas elements
2.7 flash
flash在进行 HTTP 请求时,也遵循同源策略。
但是相比较以上的各种场景和绕过同源策略的方法,flash 的跨域请求设置很容易,只需要在目标服务的根目录下设置一个crossdomain.xml
文件即可。
这个文件中会规定哪些域可以访问当前服务,看一个真实世界里的例子:
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy
SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only"/>
<allow-access-from domain="t.simple.com"/>
<allow-access-from domain="img1.simple.com"/>
<allow-access-from domain="img2.simple.com"/>
<allow-access-from domain="img3.simple.com"/>
<allow-access-from domain="img4.simple.com"/>
<allow-access-from domain="img5.simple.com"/>
<allow-access-from domain="*.simple.com.cn"/>
<allow-access-from domain="all.vic.sina.com.cn"/>
</cross-domain-policy>