同源策略和跨域

在前端开发的过程当中,咱们常常遇到"跨域"的问题,如下的文章将列举一下我在工做中碰到的跨域问题。
以及稍稍的探讨一下为何会有"跨域"问题的出现,和所谓的"同源策略"javascript

同源策略

1. 历史

1995 年由 Netscape 公司提出,以后被其余浏览器厂商采纳。html

同源策略只是一个规范,并无指定其具体的使用范围和实现方式,各个浏览器厂商都针对同源策略作了本身的实现。前端

一些 web 技术都默认采起了同源策略,这些技术范围包括但不限于Silverlight, Adobe Flash, Adobe Acrobat, Dom, XMLHttpRequestjava

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.

判断同源的三个要素:web

  • 相同的协议
  • 相同的域名
  • 相同的端口号

3. 存在的意义

为了保证使用者信息的安全,防止恶意网站篡改用户数据

举个例子:ajax

假设没有同源策略,那么我在A网站下的cookie就能够被任何一个网站拿到;那么这个网站的全部者,就可使用个人cookie(也就是个人身份)在A网站下进行操做。json

同源策略能够算是 web 前端安全的基石,若是缺乏同源策略,浏览器也就没有了安全性可言。canvas

4. 限制范围

非同源的网站之间跨域

  • 没法共享 cookie, localStorage, indexDB
  • 没法操做彼此的 dom 元素
  • 没法发送 ajax 请求
  • 没法经过 flash 发送 http 请求
  • 其余

跨域

同源策略作了很严格的限制,可是在实际的场景中,又确实有不少地方须要突破同源策略的限制,也就是咱们常说的跨域浏览器

1. cookie

同源策略最先被提出的时候,为的就是防止不一样域名的网页之间共享 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_...

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 简单请求(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-MethodAccess-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对象,可是根据同源策略,浏览器将对非同源的页面之间的windowlocation对象添加限制

不一样源的两个网页将不能:

  • 操做彼此的 dom
  • 获取/调用彼此 window 对象中的属性/方法

不一样源的两个网页能够:

  • 改变父/子级的 url

具体的规则能够参考这里:integration-with-idl

可是在现实世界中,有不少场景下,实际上是须要两个非同源的 iframe 之间进行“跨域”操做的。为了实现这种“跨域”,咱们借用了如下几种方法:

  • 片断标识符(fragment identifier)
  • 使用 window.name
  • 跨文档通讯

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.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.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文件便可。

这个文件中会规定哪些域能够访问当前服务,看一个真实世界里的例子:

<?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>

参考文章:

相关文章
相关标签/搜索