同源策略和跨域

- 7 mins

在前端开发的过程中,我们经常遇到”跨域”的问题,以下的文章将列举一下我在工作中碰到的跨域问题。 以及稍稍的探讨一下为什么会有”跨域”问题的出现,和所谓的”同源策略”

同源策略

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,但是如果两个网页的一级域名是相同的,可以通过设置 document.domain来共享 cookie。

举个例子, https://market.douban.comhttps://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 中最常用的一种跨域的方式,相比jsonpwebsoket也是最安全的一种方式。

唯一美中不足的是低版本的浏览器支持的不是很好

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

2.1.1 CORS 的运作

CROS 的设置,大部分是需要在服务端进行设置,在服务端设置之前,先来看一下 CROS 在浏览器中是怎么运作的:

首先,在浏览器中,http 请求将被分为两种 简单请求(simple request)非简单请求(not-so-simple request)

简单请求的判断包括两个条件:

  1. 请求方法必须是一下几种:
    • HEAD
    • GET
    • POST
  2. HTTP 头只能包括以下信息:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type: 只限于[application/x-www-form-urlencoded, multipart/form-data, text/plain]

不能同时满足以上两个条件的,就都视作非简单请求

2.1.2 简单请求

浏览器端

浏览器在处理简单请求时,会在 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

下面来说说这几个字段都代表什么:

2.1.3 非简单请求

简单请求最大的不同在于,非简单请求实际上是发送了两个请求。

预请求

首先,在正式请求之前,会先发送一个预请求(preflight-request),这个请求的作用是尽可能少的携带信息,供服务端判断是否响应该请求。

浏览器

浏览器发送预请求,请求的 Request Method 会设置为 options

另外,还会带上这几个字段:

服务端

服务端收到预请求之后会根据request中的origin,Access-Control-Request-MethodAccess-Control-Request-Headers判断是否响应该请求。

如果判断响应这个请求,返回的response中将会携带:

如果否定这个请求,直接返回不带这三个字段的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对象,但是根据同源策略,浏览器将对非同源的页面之间的windowlocation对象添加限制

不同源的两个网页将不能:

不同源的两个网页可以:

具体的规则可以参考这里:integration-with-idl

但是在现实世界中,有很多场景下,其实是需要两个非同源的 iframe 之间进行“跨域”操作的。为了实现这种“跨域”,我们借用了以下几种方法:

3.2 使用片段标识符(fragment identifier)

片段标识符指的就是 url 中 # 之后的部分,也就是我们常说的 location.hash。 使用片段标识符依托于以下几个关键点:

  1. 改变 url 里的这个部分,是不会触发页面的刷新的
  2. 父级页面虽然不能操作 iframe 中的 windowdom,但是可以改变 iframe 的 url
  3. 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.addEventListener('message', function (e) {
	console.log(e.source)	// 发送消息的窗口
	console.log(e.origin)  // 消息发向的网址
	console.log(e.data)    // 消息内容
})

2.6 canvas

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.comb.com是不同源的两个网页,触发了同源策略的限制。换成toBlobgetImageData会报同样的错误。

我们来探究以下报这个错误的原因:

首先,所有bitmaps类型的对象,在被canvasImageBitmap使用时,都会先检查当前这对象,是不是处在origin clean的状态。

然后,所有bitmaps类型的对象,默认情况下,这个origin clean都是true,但是如果这个bitmaps被跨域调用,那么,这个origin clean将会被设置成 false

再然后,在使用toDataURL,toBlobgetImageData时,都会先检查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文件即可。

这个文件中会规定哪些域可以访问当前服务,看一个真实世界里的例子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>

###参考文章:

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora