在前端开发的过程当中,咱们常常遇到"跨域"的问题,如下的文章将列举一下我在工做中碰到的跨域问题。
以及稍稍的探讨一下为何会有"跨域"问题的出现,和所谓的"同源策略"javascript
1995 年由 Netscape
公司提出,以后被其余浏览器厂商采纳。html
同源策略只是一个规范,并无指定其具体的使用范围和实现方式,各个浏览器厂商都针对同源策略作了本身的实现。前端
一些 web 技术都默认采起了同源策略,这些技术范围包括但不限于Silverlight
, Adobe Flash
, Adobe Acrobat
, Dom
, XMLHttpRequest
。java
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.
判断同源的三个要素:web
为了保证使用者信息的安全,防止恶意网站篡改用户数据
举个例子:ajax
假设没有同源策略,那么我在A网站下的cookie
就能够被任何一个网站拿到;那么这个网站的全部者,就可使用个人cookie
(也就是个人身份)在A网站下进行操做。json
同源策略能够算是 web 前端安全的基石,若是缺乏同源策略,浏览器也就没有了安全性可言。canvas
非同源的网站之间跨域
同源策略作了很严格的限制,可是在实际的场景中,又确实有不少地方须要突破同源策略的限制,也就是咱们常说的跨域
浏览器
同源策略最先被提出的时候,为的就是防止不一样域名的网页之间共享 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+¹ ✔ 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_...
CROS 的设置,大部分是须要在服务端进行设置,在服务端设置以前,先来看一下 CROS 在浏览器中是怎么运做的:
首先,在浏览器中,http 请求将被分为两种 简单请求(simple request)
和 非简单请求(not-so-simple request)
。
简单请求的判断包括两个条件:
请求方法必须是一下几种:
HTTP 头只能包括如下信息:
不能同时知足以上两个条件的,就都视做非简单请求
浏览器在处理简单请求时,会在 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
下面来讲说这几个字段都表明什么:
看名字大概就能猜出来,这个就是告诉浏览器,服务端接受那些域名的访问。值能够是 request
中的 origin
,也能够是 *
,也能够是originA | originB
这样的形式,可是目前看来,在浏览器中只支持单一值和*
两种方式。具体能够参考这里:access-control-allow-origin-response-header
从名字上来看,这个字段标明了是否拥有用户相关的权限。
在浏览器中,具体表现为是否能够发送 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.parent.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
文件便可。
这个文件中会规定哪些域能够访问当前服务,看一个真实世界里的例子:
<?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>