同源策略与跨域
同源策略
同源策略限制了一个源的 document 和 script 如何与另一个源交互。
源是指协议、域名、端口,同源指两个 URL 的源相同
同源策略主要限制以下三种操作
DOM 操作
首先要知道,在浏览器打开两个网页(标签/窗口),在同源时是可以相互交互的:
以本博客首页为例:源都为 https://imbant.github.io/
,在首页 /blog/
打开控制台,调用 window.open
打开 /blog/archives/
到归档,可见:window.open
会返回被打开页面的 Window
对象,而被打开的页面 window.opener
指向前一个页面的 Window
对象。
这两个暴露在其他页面的 Window 对象,是可以正常访问,执行内部方法并影响到真正的页面的:
因此可以进行 DOM 操作:
非常危险的操作。试想如果没有同源限制,打开一个钓鱼网站和银行官网,钓鱼网站可以随意获取账号密码信息。
这类允许文档间直接相互引用的 API,有
window.open()
返回被打开的 Windowwindow.opener
返回打开当前窗口的那个 Windowwindow.parent
返回当前窗口的父 Window,如果当前窗口是一个<iframe>
,<object>
, 或者<frame>
,则它的父 Window 是嵌入它的那个 Windowiframe.contentWindow
返回 iframe 的 Window
在实际操作中,浏览器提供了 window.postMessage
来更安全的实现跨域的跨文档消息机制
数据操作
无法访问不同源页面的 cookie
indexDB
localStorage
XMLHttpRequest 操作
普遍情况下,XHR 请求只可以对同源站点
XHR 跨域
为了安全性,XHR 需要牺牲便利性,来发起跨域请求。
CORS(Cross-Orign Resource Sharing)
CORS 是一个 W3C 标准,由一些 HTTP 头组成,可以让前后端共同配置,实现跨域 XHR 请求。
有两种场景:简单请求和预检请求
简单请求
简单请求需要满足:
- 方法只能为
GET
、HEAD
、POST
。其中HEAD
用于只请求 header,并且这个 header 会和GET
请求中的一样,用于下载大文件前提前了解大小,节省带宽。 - 除了默认 header 之外,只可以有以下 header
Accept
response 可选的MIME
类型Accept-Language
指定客户端可以理解的自然语言Content-Language
指定用户(audience)希望的自然语言Content-Type
- 还有一些头 TODO:
Content-Type
只能为三者之一application/x-www-form-urlencoded
通过 ‘&’ 和 ‘=’ 连接的键值对multipart/form-data
表格类型text/plain
原始文本
Content Type
的限制是为了兼容表单(form),历史上表单一直是可以跨域请求的。同时<form>
标签的enctype
属性也是这三者之一,用以标识原生表单 POST 时的 Content Type,表单不借助 js 天然就可以发出简单请求
对于简单请求,浏览器直接发出 XHR 请求,并在 header 中加入 Origin
字段,来标识本次请求的源(协议、域名、端口)。
服务端收到这个跨域 request,并且在许可的源范围内,response 中就会三个头:
Access-Control-Allow-Origin
这个字段是必须的,否则会视为跨域请求失败。值为 request 时的 Origin 或者 ‘*’Access-Control-Allow-Credentials
可选,如果为 true 表示服务端明确许可 request 中带 Cookie,并且前一个 headerAccess-Control-Allow-Origin
不能为 ‘*’,必须写明源Access-Control-Expose-Headers
指定客户端可以访问的 response 的 header,例如 response 中服务端自定义 headerX-Customer-Header
,需要指定后客户端才能在 XHR 请求对象中拿到,否则只能拿到一些基础 header TODO: 感觉很鸡肋啊,就算 JS 脚本拿不到,抓包还是能拿到没指定的 header
XMLHttpRequest 对象的请求默认不带 cookie,需要手动设置 withCredentials = true
预检请求
不符合简单请求的被称为预检(preflight)请求。
浏览器发现 XHR 请求需要预检,会先发送一个 OPTION
请求到服务端,这个请求不会对服务器资源产生影响。
用 OPTION 做预检的原因在于,节省服务端处理跨域请求的资源
预检会带上以下 header
Origin
Access-Control-Request-Method
告知实际请求的 methodAccess-Control-Request-Headers
告知实际请求携带的自定义 header
服务端确认允许跨域后,会对预检做出 response,带上 CORS header
Access-Control-Allow-Origin
同简单请求Access-Control-Allow-Credentials
同简单请求Access-Control-Allow-Methods
允许客户端使用的 method,会返回全部支持的 method,不限于预检的,避免多次预检Access-Control-Allow-Headers
允许客户端携带的 header,会返回全部支持的 method,不限于预检的,避免多次预检Access-Control-Max-Age
预检有效时间,在有效时间内发跨域请求无需再次预检
通过预检后,浏览器正常的 CORS 请求都会和简单请求一样,带上 Origin
,服务端也会每次回应 Access-Control-Allow-Origin
注意跨域请求失败的情况下,一般 response 状态码可能为 200,而 CORS 需要的 header 不存在,通过 header 感知跨域失败
简单请求 request | 简单请求 response | OPTION request | OPTION response |
---|---|---|---|
Origin |
*-Allow-Origin *-Allow-Credentials *-Expose-Headers |
Origin *-Request-Method *-Request-Headers |
*-Allow-Origin *-Allow-Credentials *-Allow-Method *-Allow-Headers *-Max-Age |
* 表示 Access-Control
前缀,方便排版
其他方式
JSONP:通过在 HTML 手动插入 <script>
标签,利用其加载资源无跨域限制的方式发送跨域请求。一般是前端全局定义一个函数,然后被请求的服务端返回一段 js 代码,去执行这个函数,并把需要的数据传给参数。缺点是只可以做 GET 请求,代码侵入性大。
本地调试时正向代理 - 服务端不知道真正发请求的客户端:node 服务转发、webpack 本地配置 devServer proxy(部署时是同源的)、Charles 代理
线上部署时反向代理 - 客户端不知道真正接收请求的服务端:nginx 负载均衡,客户端请求的是负载均衡服务器,由他转发请求到不同源
window.postMessage:见上文,更安全更受控的跨文档通信方式