一次弄懂跨域问题

25次阅读

共计 6400 个字符,预计需要花费 16 分钟才能阅读完成。

前端请求最常提到跨域问题,但是,很多开发者一直在跨域,还是搞不清楚什么是跨域?为什么要跨域?跨域方法有哪些?在我的深入研究后,写下此文,希望对你们有所帮助。

同源策略
1. 什么是同源策略?
同源策略 (Same-Origin Policy) 最早由 Netscape 公司提出, 所谓同源就是要求, 域名, 协议, 端口相同. 非同源的脚本不能访问或者操作其他域的页面对象(如 DOM 等). 作为著名的安全策略, 虽然它只是一个规范, 并不强制要求, 但现在所有支持 javaScript 的浏览器都会使用这个策略. 以至于该策略成为浏览器最核心最基本的安全功能, 如果缺少了同源策略, web 的安全将无从谈起. 并且,浏览器不是阻止请求的发送,而是对请求的拦截。
同源策略要求三同, 即: 同域, 同协议, 同端口.

同域即 host 相同, 顶级域名, 一级域名, 二级域名, 三级域名等必须相同, 且域名不能与 ip 对应;
同协议要求, http 与 https 协议必须保持一致;
同端口要求, 端口号必须相同.

注:(IE 有些例外, 它仅仅只是验证主机名以及访问协议,而忽略了端口号.)
2. 同源策略带来的问题?
同源策略下的 web 世界, 域的壁垒高筑, 从而保证各个网页相互独立, 互相之间不能直接访问, iframe, ajax 均受其限制, 而 script 标签不受此限制.
iframe 限制
可以访问同域资源, 可读写; 访问跨域页面时, 只读.
Ajax 限制
Ajax 的限制比 iframe 限制更严.
同域资源可读写;
跨域请求会直接被浏览器拦截.(chrome 下跨域请求不会发起, 其他浏览器一般是可发送跨域请求, 但响应被浏览器拦截)
Script 限制
script 并无跨域限制, 这是因为 script 标签引入的文件不能够被客户端的 js 获取到, 不会影响到原页面的安全, 因此 script 标签引入的文件没必要遵循浏览器的同源策略. 相反, ajax 加载的文件内容可被客户端 js 获取到, 引入的文件内容可能会泄漏或者影响原页面安全, 故, ajax 必须遵循同源策略.

跨域办法
使用代理
虽然 ajax 和 iframe 受同源策略限制, 但服务器端代码请求, 却不受此限制, 我们可以基于此去伪造一个同源请求, 实现跨域的访问. 如下便是实现思路:

请求同域下的 web 服务器;
web 服务器像代理一样去请求真正的第三方服务器;(服务器不存在跨域问题)
代理拿到数据过后, 直接返回给客户端 ajax.

JSONP

script 标签并不受同源策略约束, 基于 script 标签可做 jsonp 形式的访问
可以通过第三方服务器生成动态的 js 代码来回调本地的 js 方法,而方法中的参数则由第三方服务器在后台获取,并以 JSON 的形式填充到 JS 方法当中.
由于使用 script 标签的 src 属性,因此只支持 get 方法

<scriptsrc=”https://www.targetDomain.com/jsonp?callback=callbackName”></script>
postMessage
ES5 新增的 postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递. 语法: postMessage(data,origin)data: 要传递的数据,html5 规范中提到该参数可以是 JavaScript 的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候建议使用 JSON.stringify()方法对对象参数序列化,在低版本 IE 中引用 json2.js 可以实现类似效果.origin:字符串参数,指明目标窗口的源,协议 + 主机 + 端口号 [+URL],URL 会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage() 方法只会将 message 传递给指定窗口,当然如果愿意也可以建参数设置为 ”*”,这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为 ”/”。父页面发送消息:
window.frames[0].postMessage(‘message’, origin)
iframe 接受消息:
window.addEventListener(‘message’,function(e){
if(e.source!=window.parent) return;// 若消息源不是父页面则退出
//TODO …
});
其中 e 对象有三个重要的属性:

data, 表示父页面传递过来的 message
source, 表示发送消息的窗口对象
origin, 表示发送消息窗口的源(协议 + 主机 + 端口号)

CORS 跨域访问
HTML5 带来了一种新的跨域请求的方式 — CORS, 即 Cross-origin resource sharing. 它更加安全, 上述的 JSONP, postMessage 等, 资源本身没有能力保证自己不被滥用. CORS 的目标是保护资源只被可信的访问源以正确的方式访问.(目前, 主流的浏览器都支持此协议)
CORS 的请求分两种,这也是浏览器为了安全做的一些处理,不同情况下浏览器执行的操作也是不一样的,主要分为两种请求,当然这一切我们是不需要做额外处理的,浏览器会自动处理的。
简单请求(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
过程:对于简单的跨域请求,浏览器会自动在请求的头信息加上 Origin 字段,表示本次请求来自哪个源(协议 + 域名 + 端口),服务端会获取到这个值,然后判断是否同意这次请求并返回。
// 请求
GET /cors HTTP/1.1
Origin: http://api.qiutc.me
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0…
1. 服务端允许
// 返回
Access-Control-Allow-Origin: http://api.qiutc.me
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Info
Content-Type: text/html; charset=utf-8
这三个带有 Access-Control 开头的字段分别表示:

Access-Control-Allow-Origin 必须。它的值是请求时 Origin 字段的值或者 *,表示接受任意域名的请求。
Access-Control-Allow-Credentials;可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为 true,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。

再需要发送 cookie 的时候还需要注意要在 AJAX 请求中打开 withCredentials 属性:var xhr = new XMLHttpRequest(); xhr.withCredentials = true; 需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin 就不能设为 *,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie。
Access-Control-Expose-Headers 可选。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。上面的例子指定,getResponseHeader(‘Info’)可以返回 Info 字段的值。
2. 服务端拒绝当然我们为了防止接口被乱调用,需要限制源,对于不允许的源,服务端还是会返回一个正常的 HTTP 回应,但是不会带上 Access-Control-Allow-Origin 字段,浏览器发现这个跨域请求的返回头信息没有该字段,就会抛出一个错误,会被 XMLHttpRequest 的 onerror 回调捕获到。这种错误无法通过 HTTP 状态码判断,因为回应的状态码有可能是 200
2. 非简单请求
条件: 除了简单请求以外的 CORS 请求。非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。过程
1)预检请求非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。预检请求的发送请求:
OPTIONS /cors HTTP/1.1
Origin: http://api.qiutc.me
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.qiutc.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0…
“预检”请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。除了 Origin 字段,”预检”请求的头信息包括两个特殊字段。

Access-Control-Request-Method 该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT。
Access-Control-Request-Headers 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X -Custom-Header。

预检请求的返回:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.qiutc.me
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

Access-Control-Allow-Methods 必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
Access-Control-Allow-Headers 如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
Access-Control-Max-Age 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另一条预检请求。

2)浏览器的正常请求和回应一旦服务器通过了”预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。
flash URLLoder
flash 有自己的一套安全策略, 服务器可以通过 crossdomain.xml 文件来声明能被哪些域的 SWF 文件访问, SWF 也可以通过 API 来确定自身能被哪些域的 SWF 加载. 当跨域访问资源时, 例如从域 a.com 请求域 b.com 上的数据, 我们可以借助 flash 来发送 HTTP 请求.
首先, 修改域 b.com 上的 crossdomain.xml(一般存放在根目录, 如果没有需要手动创建) , 把 a.com 加入到白名单;
<?xml version=”1.0″?>
<cross-domain-policy>
<site-control permitted-cross-domain-policies=”by-content-type”/>
<allow-access-from domain=”a.com” />
</cross-domain-policy>

其次, 通过 Flash URLLoader 发送 HTTP 请求, 拿到请求后并返回;
最后, 通过 Flash API 把响应结果传递给 JavaScript.

Flash URLLoader 是一种很普遍的跨域解决方案,不过需要支持 iOS 的话,这个方案就不可行了.
WebSocket
在 WebSocket 出现之前, 很多网站为了实现实时推送技术, 通常采用的方案是轮询 (Polling) 和 Comet 技术, Comet 又可细分为两种实现方式, 一种是长轮询机制, 一种称为流技术, 这两种方式实际上是对轮询技术的改进, 这些方案带来很明显的缺点, 需要由浏览器对服务器发出 HTTP request, 大量消耗服务器带宽和资源. 面对这种状况, HTML5 定义了 WebSocket 协议, 能更好的节省服务器资源和带宽并实现真正意义上的实时推送.
WebSocket 本质上是一个基于 TCP 的协议, 它的目标是在一个单独的持久链接上提供全双工(full-duplex), 双向通信, 以基于事件的方式, 赋予浏览器实时通信能力. 既然是双向通信, 就意味着服务器端和客户端可以同时发送并响应请求, 而不再像 HTTP 的请求和响应. (同源策略对 web sockets 不适用)
原理: 为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求, 这个请求和通常的 HTTP 请求不同, 包含了一些附加头信息, 其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP 请求, 服务器端解析这些附加的头信息然后产生应答信息返回给客户端, 客户端和服务器端的 WebSocket 连接就建立起来了, 双方就可以通过这个连接通道自由的传递信息, 并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接.
参考:

跨域资源共享 CORS 详解跨域访问和防盗链基本原理 -WEB 前端 - 伯乐在线 html5 postMessage 解决跨域、跨窗口消息传递 -Samaritans- 博客园 同源策略详解及绕过(Part1)

正文完
 0