在前端开发的过程中,我们经常遇到”跨域”的问题,以下的文章将列举一下我在工作中碰到的跨域问题。 以及稍稍的探讨一下为什么会有”跨域”问题的出现,和所谓的”同源策略”
1995 年由 Netscape
公司提出,之后被其他浏览器厂商采纳。
同源策略只是一个规范,并没有指定其具体的使用范围和实现方式,各个浏览器厂商都针对同源策略做了自己的实现。
一些 web 技术都默认采取了同源策略,这些技术范围包括但不限于Silverlight
, Adobe Flash
, Adobe Acrobat
, Dom
, XMLHttpRequest
。
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.
判断同源的三个要素:
为了保证使用者信息的安全,防止恶意网站篡改用户数据
举个例子:
假设没有同源策略,那么我在A网站下的cookie
就可以被任何一个网站拿到;那么这个网站的所有者,就可以使用我的cookie
(也就是我的身份)在A网站下进行操作。
同源策略可以算是 web 前端安全的基石,如果缺少同源策略,浏览器也就没有了安全性可言。
非同源的网站之间
同源策略做了很严格的限制,但是在实际的场景中,又确实有很多地方需要突破同源策略的限制,也就是我们常说的跨域
同源策略最早被提出的时候,为的就是防止不同域名的网页之间共享 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
。
在使用 ajax 的过程中,我们碰到的同源限制的问题是最多的。
针对 ajax ,我们有三种方式可以绕过同源策略的限制:
设置 cross-domain 是目前在 ajax 中最常用的一种跨域的方式,相比jsonp
和websoket
也是最安全的一种方式。
唯一美中不足的是低版本的浏览器支持的不是很好
IE ✘ 5.5+ ◒ 8+² ◒ 10+¹ ✔ 11
Edge ✔
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_bug.cgi?id=135379
CROS 的设置,大部分是需要在服务端进行设置,在服务端设置之前,先来看一下 CROS 在浏览器中是怎么运作的:
首先,在浏览器中,http 请求将被分为两种 简单请求(simple request)
和 非简单请求(not-so-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
这个字段所携带的信息了
与简单请求
最大的不同在于,非简单请求
实际上是发送了两个请求。
首先,在正式请求之前,会先发送一个预请求(preflight-request)
,这个请求的作用是尽可能少的携带信息,供服务端判断是否响应该请求。
浏览器发送预请求
,请求的 Request Method
会设置为 options
。
另外,还会带上这几个字段:
简单请求
的origin
服务端收到预请求
之后会根据request
中的origin
,Access-Control-Request-Method
和Access-Control-Request-Headers
判断是否响应该请求。
如果判断响应这个请求,返回的response
中将会携带:
如果否定这个请求,直接返回不带这三个字段的response
就可以,浏览器将会把这种返回判断为失败的返回,触发onerror
方法
如果预请求
被正确响应,接下来就会发送正式请求,正式请求的request
和正常的 ajax 请求基本没有区别,只是会携带 origin
字段;response
和简单请求
一样,会携带上Access-Control-*
这些字段
websocket 不遵循同源策略。
但是在 websocket 请求头中会带上 origin
这个字段,服务端可以通过这个字段来判断是否需要响应,在浏览器端并没有做任何限制。
jsonp 其实算是一种 hack 形式的请求。
jsonp 的本质其实是请求一段 js 代码,是对静态文件资源的请求,所以并不遵循同源策略。但是因为是对静态文件资源的请求,所以只能支持 GET
请求,对于其他方法没有办法支持。
根据同源策略的规定,如果两个页面不同源,那么相互之间其实是隔离的。
在使用 iframe 的页面中,虽然我们可以通过iframe.contentWindow
,window.parent
,window.top
等方法拿到window
对象,但是根据同源策略,浏览器将对非同源的页面之间的window
和location
对象添加限制
不同源的两个网页将不能:
window
对象中的属性/方法不同源的两个网页可以:
具体的规则可以参考这里:integration-with-idl
但是在现实世界中,有很多场景下,其实是需要两个非同源的 iframe 之间进行“跨域”操作的。为了实现这种“跨域”,我们借用了以下几种方法:
片段标识符
指的就是 url 中 #
之后的部分,也就是我们常说的 location.hash
。 使用片段标识符依托于以下几个关键点:
window
和 dom
,但是可以改变 iframe 的 url
hashchange
事件通过这几个关键点,可以实现基于 hashchange
来操作页面
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
是否被改变
虽然上面的两种方法都可以实现不同源页面之间的通信,但是总归是属于hack
的方法,眼看着大家对非同源页面的通信都有需求,所以在 HTML5 规范中,添加了一个window.postMessage
的方法。
通过这个方法,可以方便的实现不同源的页面之间的通信。
看一个简单的例子:
// Page Foo
iframe.contentWindow.postMessage('Hello from foo', '/path/to/bar')
// Page Bar
window.addEventListener('message', function (e) {
console.log(e.source) // 发送消息的窗口
console.log(e.origin) // 消息发向的网址
console.log(e.data) // 消息内容
})
在 canvas
的使用过程中,也会碰到同源策略的限制。
以下的几种操作,都会受到同源策略的限制:
例如:
// 这段 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
flash在进行 HTTP 请求时,也遵循同源策略。
但是相比较以上的各种场景和绕过同源策略的方法,flash 的跨域请求设置很容易,只需要在目标服务的根目录下设置一个crossdomain.xml
文件即可。
这个文件中会规定哪些域可以访问当前服务,看一个真实世界里的例子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.douban.com"/>
<allow-access-from domain="img1.douban.com"/>
<allow-access-from domain="img2.douban.com"/>
<allow-access-from domain="img3.douban.com"/>
<allow-access-from domain="img4.douban.com"/>
<allow-access-from domain="img5.douban.com"/>
<allow-access-from domain="*.buick.com.cn"/>
<allow-access-from domain="all.vic.sina.com.cn"/>
</cross-domain-policy>
###参考文章: