跨域是前端开发中一个非常常见的问题,尤其是随着单页应用(Single Page Application, SPA)的兴起,前后端分离开发和部署,前端在跨域是前端开发中一个非常常见的问题,尤其是随着单页应用(Single Page Application, SPA)的兴起,前后端分离开发和部署,前端在本地开发和部署的过程中都会面临着跨域问题。我们再次聊聊跨域这个话题,以及项目中对跨域的一些实践经验,希望带来一些新的收获。
什么是跨域
首先我们需要了解下什么是同源策略,MDN 中是这样介绍的:
同源策略是浏览器的一个重要安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行操作。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
从这段话我们得知,同源策略是浏览器的安全策略,源(Origin)是判断是否满足同源策略的条件。我们常说的跨域,准确来说应该是跨源,目的是不受同源策略的限制去操作另一个源的资源。例如当我们在本地开发时,源是 http://localhost:3000
,而服务端接口的地址是 https://api.feishu.cn
,此时通过 Fetch API
访问接口就会产生跨域。
同源策略源自于浏览器,反之在非浏览器的环境下,一般是没有同源策略限制的。例如在 Node.js 中,我们可以请求任意的网址并得到结果。因此基于 Node.js 的服务端可以直接跨域访问资源。
当然我们也可以关闭浏览器的同源策略,关闭后浏览器环境下也可以直接跨域。以 Chrome 为例,通过给 Chrome 增加启动参数:
$ /path/to/chrome.app --disable-web-security
即可关闭。关闭后浏览器会有安全风险,建议只用在本地开发上
源由协议、域名(准确来说是主机名,因为除了域名也可以是 IP)、端口号共同决定,三者完全相同的两个 URL 会被认为是同源。举几个例子:
https://www.bytedance.com
和https://jobs.bytedance.com
,域名不同,不同源http://www.bytedance.com
和https://www.bytedance.com
,协议不同,不同源http://localhost
和http://localhost:8080
,端口不同(80 和 8080),不同源
特别的,当我们直接打开一个 HTML 文件时,使用的是 file
协议,请求 HTTP 资源时协议不同,会产生跨域。
源是允许被有限度的修改,浏览器提供了 API 可以将子域名下的源修改为父域名的源,例如我们在 https://open.feishu.cn
下执行:
document.domain = 'feishu.cn';
此时页面的源就变成了 https://feishu.cn
满足同源策略,我们就可以直接访问父域名下的资源。如果修改为非父域名(如 bytedance.com
),浏览器会报错。
同源策略控制不同源之间的操作,这些操作通常分为三类:
- 资源嵌入:一般是被允许的。例如
<script>
、<iframe>
引入资源 - 写操作:一般是被允许的。例如链接、重定向以及表单提交,满足特定条件的 HTTP 请求不允许
- 读操作:一般是不被允许的。例如
XMLHttpRequest
和Fetch API
发起 GET 请求
根据这个分类,我们来重点讨论几种情况:
<script>
是资源嵌入不受同源策略控制,JSONP 就是借助这一特性实现的跨域<form>
是资源写入不受同源策略控制,因此表单的action
不是同源 URL 时也可以提交成功XMLHttpRequest
或Fetch API
是我们重点关注的,下面我们具体讨论下:- 当发起 HTTP
GET
请求(也可以是其它类型)读取资源,受同源策略控制,请求返回的内容在不同源的情况下我们是读不到的 - 当发起 HTTP
POST
请求修改资源,一般是不受同源策略控制的(称为简单请求),资源是允许被修改;而对于非简单请求,受同源策略控制,资源不允许修改
- 当发起 HTTP
需要特别说明的是:
- HTTP
GET
读取资源虽然受同源策略控制,但请求是成功发送的,只是浏览器限制了请求返回的内容不给我们而是抛出了错误 - HTTP
POST
写入资源时,简单请求也是发送成功的,因此资源被成功修改了,因此可以说写操作不受同源策略控制,但请求的返回值我们同样获取不到(因为是读操作) - 非简单请求采用了先发一个预检请求的方式,判断是否允许跨域,不允许则不会发送真实的请求,避免资源被修改,因此受同源策略控制。下面实践部分会详细讨论如何允许跨域。
WebSocket
不受同源策略控制
简单请求必须满足以下条件:
- HTTP 方法是:
GET
POST
或HEAD
- 除了被浏览器自动设置的字段(例如
Connection
、User-Agent
),请求头只允许以下字段:Accept
Accept-Language
Content-Language
Content-Type
:只允许值为text/plain
multipart/form-data
application/x-www-form-urlencoded
Range
:只允许简单的范围标头值,如bytes=256-
或bytes=127-255
不满足上述条件的即为非简单请求。例如当我们在请求头增加 X-JWT-Token
或 Content-Type: application/json
时,这个请求就是非简单请求。
为什么需要同源策略
上面我们说了,同源策略是一个安全策略。如果同源策略被关闭,个人信息将会有安全风险,例如:
- 恶意站点可以调用其它网站的用户信息接口,获取敏感信息
- 恶意站点可以调用其它网站的点赞或删除接口,恶意修改资源
跨域的最佳实践
那么我们如何才能跨域呢?浏览器在同源策略的基础上,提出一种可以安全的跨域机制称为跨源资源共享(Cross Origin Resource Sharing,CORS)。当然在这个机制发布之前,也有 JSONP
等满足浏览器安全机制的跨域方案,本文不具体讨论。
CORS 的使用很简单,服务端(即被请求的资源)在响应头中增加:
Access-Control-Allow-Origin: https://www.bytedance.com
即可,其中的值表示允许跨域访问的源,此时 https://www.bytedance.com
就可以访问这个服务器的