术语
同源
请求URL拥有相同protocol、host、port。参考Table 1 同源举例。
用户认证(User Credential)
在CORS中,用户认证指cookies, HTTP基本认证,客户端SSL认证。不指代理端的认证或Origin头。
缺乏用户认证是指响应中不包含cookie,而且请求中不包含HTTP认证和SSL证书。
简单方法(Simple method)
指的是GET, HEAD, POST这三个method。
简单头
指Accept, Accept-Language, Content-Language, Content-Type满足application/x-www-form-urlencoded,multipart/form-data或text/plain。
简单响应头
指的是下列几个header字段:Cache-Control, Content-Language, Content-Type, Expires,Last-Modified, Pragma。
背景
为了网络安全,用户代理(即浏览器)对客户端Web应用程序应用了同源政策,一个域的脚本不能访问其他域的document。这样的好处是虽然脚本可以欺骗用户打开另一个域的敏感页面,但可以防止脚本读取敏感数据。
缺点就是给资源共享带来了困难,跨域数据访问的目的就是实现跨域的资源共享。
理解同源政策
Script通过src引用,Script的域就是引用它的页面所在的域,与Script原来在哪无关。
同源政策:Prevents access to DOM on different sites.
理解:假设脚本Script在域A.com中,然后在B.com中有一个页面通过src引用了A.com的Script。这个Script可以访问B.com中页面的任何内容,但如果Script打开一个新窗口并加载A.com或其他域的页面,根据同源政策,Script不能访问A.com或其他域中页面的内容或属性。虽然可以打开新窗口(并加载其他域的页面)或关闭这个窗口,但无法访问窗口内页面的内容。
解决办法一 document.domain
如果两个窗口(windows or frames)各自有自己的脚本同时把document.domain设置为相同的值,即使不是同源的,两个窗口依然可以相互访问。
解决办法二 JSONP(JSON withPadding)
JSONP是一种跨域手段。
由于同源策略,不同域的网页不能进行数据沟通,而HTML的script元素是个例外,script元素可以加载不同域的脚本。如果加载的不是脚本,而是动态的JSON资料,则这个就是JSONP了。用script抓取的并不是JSON,而是任意的JavaScript,因此是由JavaScript直译器执行,而不是用JSON解析器解析。
原理
把<script>元素的src属性设成一个回传JSON的URL是可以想象的,这也代表从HTML页面通过script元素抓取JSON是可能的。
问题是JSON文件不是JavaScript程序,无法执行,从src里URL回传的必须是可执行的JavaScript。在JSONP的使用模式里,这个URL回传的JSON是由函数呼叫包起来的,这就是JSONP的“填充(padding)”或是“前缀(prefix)”的由来。
惯例上浏览器提供回调函数的名称当作送至服务器的请求中命名查询参数的一部分,例如:
<scripttype="text/javascript" src = "http://server2.example.com/RetrieveUser?UserId=1823&jsonp=parseResponse">
</script>
服务器会在传给浏览器前将 JSON 回应用这个前辍(或称作填充)包起来。浏览器得到的回应已不是单纯的资料叙述而是一个脚本。在本例中,浏览器得到的是:
parseResponse({"Name":"Cheeso", "Id" : 1823, "Rank": 7})
填充
填充不一定是浏览器页面中已定义的某个回调函数,也可以是变量赋值,if叙述或其它Javascript叙述。回传内容可以是任意的运算式,甚至不需要任何的JSON,不过惯例上填充部分还是会触发函数调用的一小段JavaScript片段,而这个函数呼叫是作用在JSON格式的资料上的。
Script元素“注入”
需要为每一个JSONP请求加一个新的、有所需src值的script元素到HTML中。
安全问题
从其它域返回的函数或脚本可以访问该域下任何数据,这有安全问题。为了达到安全的要求,于是定义了更严格的JSON-P,使浏览器可以对MIME类型是"application/json-p"的请求做强制处理,如果回应不能被解析为严格的JSON-P,浏览器可以丢出一个错误或忽略整个回应。
JSON-P严格模式
就是强制使返回的JSONP满足这个约束:
functionName({JSON});
obj.functionName({JSON});
obj["function-name"]({JSON});
解决办法三 CORS (Cross-Origin ResourceSharing)
这是W3C从2005年开始起草的一个标准,专门用于解决跨域请求。
原理
用户代理通过访问HTTP响应返回的头信息来授权认证该资源是否可以访问当前域。
跨域请求信息
从当前域的页面给另一个域的URL发送请求,用户代理会在请求中添加请求头Origin表示发送请求的域。另外还有请求头Referer表示发送请求页的URL。
服务器应用程序可以根据请求头Origin判断请求是不是跨域请求,并作出反应是否什么也不返回(响应头是要返回的,只是可能没有响应体)。
跨域响应信息
Web应用程序如果允许其他域的应用程序访问,服务器可以在响应中添加响应头Access-Control-Allow-Origin,表示允许访问的域。用户代理(浏览器)检测到这个响应头之后会比较请求的域,如果请求域属于被允许访问的域,则发送请求的域可以访问响应中的内容。
另外,还可以在响应中添加其他响应头控制允许访问的时间长度,允许使用什么方法访问。
使用经验
1. 如果某个资源对其它域没用(比如登陆页面),就不需要返回Access-Control-Allow-Origin头。要保护自己被CSRF攻击,还需要要求在请求中添加一个token来做判断。
2. 不需要做访问控制检查的页面,可以返回Access-Control-Allow-Origin为"*"。
3. 一个GET响应的整个内容都可以解析成ECMAScript,可以返回Access-Control-Allow-Origin为"*"。
有用户认证或Origin头的请求需要特殊考虑:
1. CORS是JSONP,Server-to-Serverback channel,cross-document messaging的替代品。但这个替代也有自己的缺陷,比如,与Server-to-Server back channel相比,被请求的资源有更高的特权。与JSONP相比,则有很好的优越性,因为JSONP是通过注入代码来完成跨域的。与cross-document messaging相比,则差不多,因为对于不完全信任的域的资源还是要做格式验证或值验证。
一般请求语法
通过设置请求头或响应头的方式,允许不同域的脚本可以访问当前域的数据。
请求头
Origin
表明请求的来源。
响应头
Access-Control-Allow-Origin
允许访问的域名,“*”号,“null”,或空格分开的字符串。如果请求的域匹配这个响应头中某个域名,则请求页面可以访问响应的内容。
Preflight请求和响应
有些浏览器会先发一个请求到另一个域的服务器中,告诉服务器请求大概会是什么样子,以询问服务器是否支持这样的跨域。这就叫preflight request。
preflight请求头
Access-Control-Request-Method
表明请求将通过何种方法(post, get和head)去获取目标域的数据。
Access-Control-Request-Headers
表明请求将所包含的头信息。
preflight 响应头
Access-Control-Allow-Methods
告诉请求域,服务器只支持使用这些方法访问这个域。
Access-Control-Allow-Headers
告诉请求域,你只能在请求中添加这些请求头。
Access-Control-Allow-Credentials
如果preflight请求暗示实际的请求可能包含用户认证,这个头的信息表示在缺乏用户认证的情况下,是否暴露响应。
"Access-Control-Allow-Credentials"":" true
true: %x74.72.75.65; "true", case-sensitive
解决办法四 XDomainRequest
XDomainRequest是IE8和IE9提供的一个CORS实现,IE10又被删除了。
使用XDomainRequest需要满足两个条件:
- 请求源的安全协议必须和被请求的URL的协议一致,即http对http,https对https,否则请求返回"Access is Denined"。
- 请求的服务器必须在响应中设置Acces-Control-Allow-Origin头保证请求源可以访问这个服务器。
属性 | 解释 |
timeout | 设置请求超时时间 |
responseText |
|
方法 | 解释 |
open(method, url) | 打开一个请求,GET/POST |
send(data) | 发送请求 |
abort() | 放弃请求 |
事件处理 | 解释 |
onprogress |
|
ontimeout |
|
onerror |
|
onload |
|
索引
同源举例
Table 1 同源举例
Compared URL | Outcome | Reason |
httpː//www.example.com/dir/page2.html | Success | Same protocol and host |
httpː//www.example.com/dir2/other.html | Success | Same protocol and host |
httpː//username:password@www.example.com/dir2/other.html | Success | Same protocol and host |
httpː//www.example.com:81/dir/other.html | Failure | Same protocol and host but different port |
https://www.example.com/dir/other.html | Failure | Different protocol |
http://en.example.com/dir/other.html | Failure | Different host |
http://example.com/dir/other.html | Failure | Different host (exact match required) |
http://v2.www.example.com/dir/other.html | Failure | Different host (exact match required) |
httpː//www.example.com:80/dir/other.html | Don't use | Port explicit. Depends on implementation in browser. |
postMessage
HTML5中最酷的新功能之一就是 跨文档消息传输CrossDocument Messaging。 下一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 Facebook已经使用了这个功能,用postMessage支持基于web的实时消息传递。
otherWindow.postMessage(message,targetOrigin);
otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性;window.open的返回值;通过name或下标从window.frames取到的值。
message: 所要发送的数据,string类型。
targetOrigin: 用于限制otherWindow,“*”表示不作限制
a.com/index.html中的代码:
<iframe id="ifr" src="b.com/index.html"></iframe> <script type="text/javascript"> window.onload = function() { var ifr = document.getElementById('ifr'); var targetOrigin = 'http://b.com'; // 若写成'http://b.com/c/proxy.html'效果一样 // 若写成'http://c.com'就不会执行postMessage了 ifr.contentWindow.postMessage('I was there!', targetOrigin); }; </script>
b.com/index.html中的代码:
<script type="text/javascript"> window.addEventListener('message', function(event){ // 通过origin属性判断消息来源地址 if (event.origin == 'http://a.com') { alert(event.data); // 弹出"I was there!" alert(event.source); // 对a.com、index.html中window对象的引用 // 但由于同源策略,这里event.source不可以访问window对象 } }, false); </script>