在制作oneday-music-player的时候要使用ajax向百度音乐的api发送请求,然后出现了XMLHttpRequest cannot load 'http://....' . No 'Access-Control-Allow-Origin' header is present on the request resource. Origin 'http://....' is therefore not allowed access,经过搜索发现是受到了同源策略的影响而导致的跨域问题,所以学习一下关于跨域的知识点。

同源策略

同源策略限制从一个源加载的文档或脚本与另一个源的文档或脚本进行交互的方式,是隔离潜在恶意文件的重要安全机制。

两个页面拥有同样的协议、端口(如果指定)和域名时,可以说两个页面是同源的。

下表是相对于http://store.company.com/dir/page.html同源检测的示例:

url 结果 原因
http://store.company.com/dir2/other.html 成功
http://store.company.com/dir/inner/other.html 成功
https://store.company.com/secure.html 失败 不同协议(httpshttp
http://store.company.com:81/dir/etc.html 失败 不同端口(81和80)
http://news.company.com/dir/other.html 失败 不同域名(newsstore

而如果非同源,则有三种行为会受到限制:

  • Cookie、LocalStorage和IndexDB无法读取
  • DOM无法获得
  • AJAX请求不能发送

规避同源策略(跨域)

Cookie

document.domain

Cookie是服务器写入浏览器的一小段信息,只有同院的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过document.domain共享Cookie

例如,假设文档中的一个脚本在http://store.company.com/dir/page.html执行以下语句:

document.domain = "company.com"

此时,http://news.company.com/dir/other.htmlhttp://store.company.com/dir/other.html
就可以通过document.cookie来设置或获取Cookie,即共享Cookie。

但是这种方法适用于Cookie和iframe窗口,LocalStorage和IndexDB无法通过这种方法规避同源策略。

iframe

如果两个网页不同源,就无法拿到对方的DOM,典型的例子是iframe窗口和window.open方法打开的窗口,如果和父窗口不同源,则会报错。

此时如果两个窗口一级域名相同,只是二级域名不同,那么设置document.domain属性,就可以规避同源策略。

而对于完全不同源的网站,目前有三种方法可以解决跨域窗口之间的通信问题。

  • 片段标识符(fragment identifier)
  • window.name
  • 跨文档通信API(cross-document messaging)

片段标识符

片段标识符(fragment identifier)指的是URL的#后面的部分,即http://store.company.com/dir/other.html#fragment#fragment(location.hash),如果只改变片段标识符,页面不会重新刷新。

父窗口可以把信息写入子窗口的片段标识符,子窗口通过监听hashchange事件得到通知。

window.name

每个iframe都有包裹它的window,这个window是top window的子窗户,所以自然有window.name属性,指的是当前窗口的名字,这个属性的最大特点是,无论是否同源,只要在同一个窗口里,窗口内所有页面对window.name都有读写的权限。

window.name的值只能是字符串的形式,这个字符串的最大能允许2M左右甚至更大的一个容量,具体取决于不同的浏览器。

例如,想要在http://example/a.html中获取http://company.com/data.html中的数据,可以在a.html中使用一个隐藏的iframe,将iframe的src首先设置为http://company.com/data.html,将其window.name设置为所需的数据内容,随后再将这个iframe的src设置为跟a.html页面同一个域的一个页面,不然a.html获取不到该iframe的window.name

window.postMessage

这是html5中新引入的一个API,可以使用它向其它的window对象发送消息,无论这个window对象属于同源还是不同源。

例如,父窗口http://example/a.html向子窗口http://company.com/data.html发送消息:

var newWin = window.open('http://company.com/data.html', 'title')
newWin.postMessage('Hello World!'. 'http://company.com/data.html')

window.postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源,即协议+端口+域名,也可以设置为*,表示不限制域名。

子窗口向父窗口发送消息的写法类似:

window.opener.postMessage('Nice to see you', 'http://example/a.html')

子窗口和父窗口都可以通过message时间,监听对方的消息。

window.addEventListener('message', function(e) {
    // ...
}, false)

message事件的事件对象event有以下三个属性:

  • event.source: 发送消息的窗口
  • event.origin: 消息发向的网址(可以限制目标网址)
  • event.data: 消息内容

通过window.postMessage,也可以读写其他窗口的localStorage

AJAX

同源策略规定,AJAX请求只能发给同源的网址,否则就报错,但是有三种方法可以规避这个限定:

  • JSONP
  • WebSocket
  • CORS

JSONP

JSONP是服务器与客户端跨源通信的常用方法。基本思想是利用<script>请求脚本能够跨域访问的特性,先定义了一个回调方法,然后将其作为url参数的一部分发送到服务端,服务端通过字符串拼接的方式将数据包裹在回调方法中,再返回回来。

// 网页动态插入`<script>`元素
function addScriptTag(src) {
    var script = document.createElement("script")
    script.setAttribute("type", "text/javascript")
    srcipt.src = src
    document.body.appendChild(script)
}

window.onload = function() {
    addScriptTag('http://example.com/ip?callback=foo')
}

function foo(data) {
    // ...
}

WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务支持,就可以通过它进行跨源通信。

浏览器发出的WebSocket请求的头信息中含有Origin字段,表示该请求的请求源,即发自哪个域名。(加入白名单)

CORS

跨域资源共享(Cross-Origin Resource Sharing,CORS)是一种使用额外的HTTP头来使一个用户代理从一个不同于当前站点(域)的服务器获取指定的资源的机制。用户代理使用跨域HTTP请求来获取与当前文档不同域、不用协议或端口的资源。

出于安全考虑,浏览器会限制从脚本内发起的跨域HTTP请求。而跨域资源共享(CORS)机制允许web应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。浏览器支持在API容器中(例如XMLHttpRequestFetch)使用CORS,以降低跨域HTTP请求所带来的风险。

跨域资源共享标准允许在下列场景中使用跨域HTTP请求:

  • 由XMLHttpRequest或Fetch发起的跨域HTTP请求;
  • web字体(CSS中通过@font-face使用跨域字体资源),因此,网站就可以发布TrueType字体资源,并只允许已授权网站进行跨站调用;
  • WebGL贴图;
  • 使用drawImage将Images/video画面绘制到canvas;
  • 样式表(使用CSSOM);
  • Scripts(未处理的异常)。

浏览器将CORS请求分成两类:简单请求(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/from-data、text/plain)

只要不同时满足上面两个条件,就属于非简单请求

简单请求

对于简单请求,浏览器会在请求头部增加一个Origin字段。这个字段用来说明本次请求来自哪个源(协议+域名+端口)。服务器根据这个值决定是否同意这次请求。

如果Origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应。而这个回应的头信息不包含Access-Control-Allow-Origin字段,从而会抛出错误被XMLHttpRequestonerror函数捕获,(回应的状态码有可能是200)。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段

Access-Control-Allow-Origin: ...
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: callback
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin: 必须。值要么是请求时Origin的值,要么是'*'
  • Access-Control-Allow-Credentials: 可选。布尔值,决定是否允许发送Cookie,不需要则删除该字段。
  • Access-Control-Expose-Headers: 可选。CORS请求时,XMLHttpRequest对象的 getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就需要在Access-Control-Expose-Header里面指定。上面的例子指定为callback,则可以使用getResponseHeader(callback)获取callback字段的值。

CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发送到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段,另一方面,开发者需要在AJAX请求中设置withCredentials属性:

var xhr = new XMLHttpRequest()
xhr.withCredentials = true

否则,即使服务器同意发送Cookie,浏览器也不会发送。

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源策略,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且跨域的原网页中的document.cookie操作也无法获取嗷服务器域名下的Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为“预检”请求。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP操作和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

“预检”请求用请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

还有以下两个特殊字段:

  • Access-Control-Request-Method: 必须。列出非简单请求的请求类型
  • Access-Control-Request-Headers: 非简单请求额外携带的头信息字段。

服务器返回的响应:

Access-Control-Allow-Methods: ...
Access-Control-Expose-Headers: callback
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
  • Access-Control-Allow-Methods: 必须。逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。为了避免多次“预检”行为。
  • Access-Control-Expose-Headers: 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
  • Access-Control-Allow-Credentials: 与简单请求时的含义相同。
  • Access-Control-Max-Age: 本次预检请求的有效期。

CORS与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

参考资料

Same origin policy

浏览器同源政策及其规避方法

js中几种实用的跨域方法原理详解

跨域资源共享 CORS 详解

CORS


oneday
279 声望4 粉丝