1. 同源与跨域
1.1 基调
一般情况下,禁止一个域从另一个域读取数据,却可以使用某些从其他域拿到的资源。比如说,允许一个域执行、渲染、应用从其他域获取到的脚本、图片、样式;同样,一个域可以展示从其他域获取的内容,比如在 frame 中显示 html 文档。网络资源也可以选择性的让其他域来读取自己的信息,比如使用 Cross-Origin Resource Sharing,这种情况下访问权是针对单个域授权的。
同源策略限制消息从一个域发送到另一个域。比如说同源策略允许域间的 GET 和 POST 方式的 HTTP 请求,却禁止域间的 PUT 和 DELETE 方式的请求。同时,域在发送请求到自己时可以自定义 HTTP 请求头,发送请求到其他域不能自定义请求头。
同源策略的控制者是浏览器,浏览器可以控制不同域之间的资源的访问或相互操作,但不控制自己对不同域之间的资源的操作和访问。
1.2 什么是源
RFC6454 规定一个资源的源由资源的 URI 中的(协议,主机,端口)这一个三元组来确定(IE 中没有把端口纳入源的属性)。比如 https://www.test.com/test-script.js, 这个资源的源为(https, www.test.com, 80)。
对于一个被执行的脚本来说,他的源属性是执行这个脚本的资源的源属性。比如一个页面加载了来自另一个域的脚本,这个脚本执行的时候向页面所在域发送请求被视为同源。
相对路径和无法明确源属性的协议(javascript:,data:,about:blank)的资源的源,取将这些资源载入的页面的源。
IE 没有将端口作为同源的组成部分,原因是 IE 历史垄断的市场占有率导致的历史遗留问题,IE8 尝试在原生的 XMLHTTPRequest 中使用端口作为同源的一部分(标准化),但效果不好,一些依照老 IE 特性开发的站点,为了保持兼容性,继续使用了原有的 MSXML 方式的 XMLHTTPRequest。所以在 IE8-11 的版本都没有考虑端口作为同源的判断条件。
1.3 怎样算同源
两个资源的组成源的属性(协议,主机,端口),完全一致,这两个资源才同源。这里如果两个资源的协议分别为 http 和 https 被认为不同源。同时,即使两个资源的所属同一台服务器,但给出的域名分别为 www.testA.com 和 www.testB.com,这两个资源也被认为是不同源。
1.4 同源策略
同源策略是浏览器的核心安全策略,目的是将来自不同源的资源进行隔离,并控制不同源资源间的通信,从而减少安全威胁,增强安全性。
1.4.1 同源策略的规则
a. 不限制
互联网的核心思想是资源共享,资源的相互访问应该被允许。
- 执行来自其他域的脚本(如使用
<script>
标签引用 CDN 的脚本,JSONP 请求等); - 渲染来自其他域的图片(如使用
<img>
标签引用图片服务器的图片资源); - 应用来自其他域的样式(如使用
<link>
标签引用来自静态资源服务器的样式文件); - 嵌入来自其他域的资源(使用
<iframe>
,<frame>
加载来自其他域的资源); - 重定向页面地址(
Location
对象地址的改变,使用<a>
链接到其他资源); - 数据发送(使用
<form>
向其他域提交数据); - 多个子域的资源可以设置
document.domain
来改变所属域属性,来实现子域同域; window.name
属性是可写的,脚本可以随意设置,iframe
的name
属性,会作为iframe
内部window
对象的name
属性初始值;- 窗体层级嵌套可以通过
parent.parent...
的方式来访问祖先窗口,不受跨域限制; postMessage
接口跨域通信;<video>
、<audio>
、<object>
、<embed>
、<applet>
、@font-face
可以加载其他域的资源。
b. 部分限制
- 跨域发送请求不能自定义 HTTP Header;
- 跨域发送请求不能使用 PUT 和 DELETE 方式,只能使用 GET 和 POST;
- 脚本可以访问一个不同源窗口的整体,而不能访问窗口的内部信息;
- CORS 可以改变跨域的情形
Access-Control-Allow-Origin
,同源策略不放宽,跨域请求正常工作(设置为例外),不包含用户名和密码,不包含cookie
和token
,响应的cookie
会被丢弃,如果需要这些信息,需要设置XMLHttpRequest
的withCredentials=true
。
c. 完全限制
- 限制本地文件系统读写;
- 限制 cookie 的访问;
- 限制 FileUpload 元素的 value 属性,不能修改,甚至不能读取路径;
- 限制脚本对来自不同服务器的文档的读写(同源策略);
- 限制本地存储 localStorage 和 sessionStorage 和 IndexedDB;
- 限制 XMLHTTPRequest 请求的发送对象。
1.5 跨域
同源策略将来自不同源的资源进行了隔离,略在阻止安全威胁的同时,也对正常的访问带来了不便,我们也需要在安全策略下根据应用的需要进行不同源资源间的通信。 比如和 iframe 或 frame 中的资源的通信,与新窗口的资源的通信,与不同域的服务器之间的通信等。
比如同源策略限制了不同源的资源之间的互操作,当页面在 iframe 中加载不同源的内容的时候,想要根据页面内容动态调整 iframe 的高度的时候,就需要特殊的跨域操作。
在应用系统中做单点登录的时候,在进行当前应用的认证的时候需要向认证中心进行认证,这个时候也需要特殊的操作。
2. 跨域方法汇总
跨域的方法和浏览器安全问题都围绕着同源策略来展开,我们可以避开浏览器端的参与,从而规避同源策略带来的不便;同时我们也可以利用同源策略及其辅助接口开放的功能特性来实现跨越通信。
2.1 惹不起躲得起
如果可以的话,可以将 Web 应用部署在同一个域下,这样可以很好的回避跨域的问题,我们常用的通过本域的后端接口包装,避免跨域的问题。
2.2 使用反向代理
直接使用 Web 服务器 apache,nginx 等的反向代理的方式,将需要跨域的请求发送到当前域,在 Web 容器配置中做请求转发,像 nginx 这样的反向代理很擅长做这样的事情。
2.3 直面惨淡的人生
2.3.1 动态不受限标签
使用脚本动态创建<script>
、<img>
、<link>
、<iframe>
、<frame>
等标签,在加入文档 DOM 后,浏览器会自动加载并解析渲染响应的资源。
2.3.2 JSONP
JSONP 原理其实是对<script>
标签的一个利用,首先<script>
标签加载资源是不受域限制的,然后浏览器会将<script>
加载的内容当做脚本来执行。如果服务端返回的是类似于callback({"data":[{"aa":1}]})
这样的内容,那么浏览器会将{"data":[{"aa":1}]}
作为函数参数调用callback
这个方法。这样就实现了从不同域加载数据。
2.3.3 Form 提交
Form 提交不受限制,客户端可以以 GET 和 POST 的方式向服务端提交数据。
2.3.4 document.domain
每一个窗体可以对当前窗体所属域进行微调,比如当前与名为app.test.com
,则可以设置document.domain
为test.com
,也可以设置为app.test.com
。通过将子域的document.domain
属性均改为主域test.com
,可以实现test.com
下的任意子域app.test.com
、auth.test.com
、img.test.com
等之间的通信。
修改为主域之后,子域的访问会带上父域的cookie
,反之则不然 .test.com
和 test.com
效果一样,写成 test.com
浏览器会理解为.test.com
。
2.3.5 window.name
window.name
在加载不同的页面后还会存在,可以通过使用同一个window
来加载需要通信的页面,通过共享window.name
来进行数据通信。
2.3.6 CORS(Cross Origin Resource Sharing)
通过协商的的 HTTP Header 让浏览器和服务端进行通信,来决定请求或者响应是否有效。
默认情况下,浏览器发送跨域请求不带认证信息(比如 cookie、证书、代理认证信息等),withCredentials
属性值为false
跨域需要withCredentials=true
,同时服务端允许Access-Control-Allow-Credentials:true
,同时Access-Control-Allow-Origin
值不能为*
。
2.3.7 postMessage
这个是 HTML5 新增的页面间通信的接口,能够很好的解决 iframe 之间通信的问题。
2.4 Fetch
2.4.1 带认证信息信息跨域
- 请求设置
credentials:true
; - 响应设置
Access-Control-Allow-Origin:http://origin.to.cross,Access-Control-Allow-Credentials:true
。
2.4.2 请求对象
mode
值为 same-origin,cors,no-cors(默认),navigate,websocket- credentials mode 值为 omit(默认),same-origin,include
2.4.3 响应对象
- 同域响应 type 值为 basic,cors,default(默认),error
- 跨域响应 type 值为 opaque,opaqueredirect,error
2.4.4 WebSocket
这也是 HTML5 新增的浏览器和服务端通信的非 HTTP 通信的机制,它不受同源策略的限制,是解决跨域数据传输的解决方案。
- 在 https 的页面,无法发送 ws://的请求,同 http
3. 不同角度看问题
3.1 本地页面间通信 VS Browser-Server 通信
3.1.1 本地页面间通信
- 动态标签
- postMessage
- window.name
- document.domain
3.1.2 Browser-Server 通信
GET
- 动态标签
- JSONP
- Form 提交
- CORS
- WebSocket
- fetch
POST
- Form 提交
- CORS
- WebSocket
- fetch
3.2 单向通信 VS 双向通信
单向通信
- 动态标签
- JSONP
- Form 提交
- CORS
- fetch
双向通信
- window.name
- document.domain
- postMessage
- WebSocket
3.3 前端单独处理 VS 需要后端配合
前端单独处理
- 动态标签
- window.name
- document.domain
- postMessage
- fetch
需要后端配合
- JSONP
- Form 提交
- CORS
- WebSocket
3.4 全版本浏览器 VS 现代浏览器
全版本浏览器
- 动态标签
- JSONP
- Form 提交
- window.name
- document.domain
现代浏览器
- 动态标签
- JSONP
- Form 提交
- window.name
- document.domain
- CORS
- postMessage
- WebSocket
- fetch
参考资料
- RFC 6454 The Web Origin Concept
- W3C 同源策略
- MDN CORS
- 《Web 之困-现代 Web 应用安全指南》
本文来自 Qunar 技术沙龙
作者:张钌,去哪儿网旅游度假事业部前端开发工程师,主体是 JavaScript 和 Node 相关技术;搞过 Java、PHP、Python 相关的 Web 开发,混过安全圈;对性能调优和安全攻防感兴趣,侧重基础理论(现阶段是操作系统和编译器原理),专注效率改善,让一切技术高效合理。