跨域是前端工程中一个很常见的问题。今天就展开来聊聊跨域,以及常见的处理方式和原理。
什么是跨域?
跨域这个概念源于浏览器对 JavaScript 的一种安全限制,我们通常称之为同源策略。默认情况下,我们只能访问同一个协议、域名和端口下的资源。在现代的前后端分离的架构中,前端需要调用大量的后端接口,因此,解决跨域问题就变得尤为重要。
为什么会有跨域?
产生跨域的原因主要有以下两个原因
- 浏览器安全限制(不能读取不同域、端口、协议下的内容)
- 使用的是 XHR(XMLHttpRequest)请求
同源策略是一个众所周知的安全策略,它确实有效地预防了一些安全问题。然而,它同时也阻止了许多有用的跨域请求。而 XHR 受到同源策略的限制:浏览器不允许 JavaScript 查找跨域文档的内容。由于 XHR 使用 responseText
属性暴露文档内容,因此同源策略不允许 XHR 进行跨域请求。
解决跨域的方案
- JSONP
- 跨域资源共享(CORS)
- Nginx
- dev-server
JSONP
前文我们说过, 产生跨域的原因之一是 XHR 请求, 但是 script
发出的请求类型(type)并不是 xhr
,因此可以解决跨域的问题。
JSONP 由回调函数和数据组成的, 实现方式就是动态创建一个<script>
标签, 然后设置src
属性指向的跨域的 URL(包含请求参数)来向服务端请求数据。
比如我们要查询小明的信息, 这时我们得知它的 userID
为 1150, 同时我们都知道 GET 请求可以通过 url 进行传参, 因此我们向服务器发起请求:
1 | var script = document.createElement('script') |
插入<script>
标签到<body>
后, 浏览器立马就去请求服务器的资源. 值得注意的是, 使用jsonp
也需要服务端的配合. 因此必须通过某种方式来告知服务端, 我们正在通过<script>
标签调用请求, 必须返回一个 JSONP 响应, 而不应该是普通 JSON 响应.
至于什么叫jsonp
响应呢? 这里其实很好理解:
假设后端发回来的是json
格式的数据, 我们也用不了呀, 数据还是数据, 不会做任何变化.. 为了让浏览器可以在<script>
标签里直接使用, 我们需要让服务端返回一段js代码 —— 用函数包装的json的形式(这也jsonp中”P(padding)”的含义). 这个函数名前后端可约定. 如下:
1 | // 服务端返回 js 代码到 <script>里 |
我们来拿B站为例. 打开chrome下的network, 上图就是jsonp
的应用, 服务端返回的js脚本. 下图可以发现, 我们发出去的请求类型是 script
, 验证了前文所说的 <script>
不受同源策略影响的.
目前主流的类库都对 jsonp
进行了封装, 如 JQuery
的 getJSON
和 ajax
, 这里就不深入讲解了. 最后对 jsonp
总结一下:
jsonp
实际上是一个非正式传输协议, 或者说是一种”投机取巧”的方式. 我们可以利用<script>
的特性从而进行数据交互解决跨域的问题. 相对来说, 它也有一定的局限性: 只能应用在 GET 请求上, 除此之外还有安全性的问题 —— 只能用在我们信任的服务端, 因为你不能保证对方未来会给你传些什么…
跨域资源共享(CORS)
概述
说完了”不正规”的jsonp
, 紧接着我们再说说原生的CORS
规范. 我们先来看看官方的定义:
CORS(Cross-origin resource Sharing, 跨资源共享), 定义了访问跨域资源时, 浏览器和服务器应该如何沟通. 其背后主要思想就是使用自定义的HTTP头部来让浏览器与服务器进行沟通, 从而决定请求或相应是否成功, 还是应该失败.
目前主流的浏览器都已经对CORS有着良好的支持, 而IE8 ~ 9则还需要使用专用的XDomainRequest
这里我们抛开不谈.
这个功能实际上是由浏览器自动完成的, 我们并不需要做什么额外的工作. 对于开发者来说, 也就需要了解一些安全细节的问题, 这一点我们放在后面讲.
两种请求
浏览器发送CORS请求时, 会将请求分为简单请求与非简单请求.
在我们日常工作中, 常用的简单请求可以将其归为以下几点:
- 使用的方法(Methods)为
HEAD
、GET
、POST
- 请求头无自定义头
Content-Type
只能是以下几种- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
非简单请求:
PUT
,Delete
方法的 ajax 请求- 发送 JSON 格式的 ajax 请求(比如post数据)
- 带自定义头的 ajax 请求
如果是简单请求, 则会先执行, 后判断。执行的过程大致如下:
浏览器发起请求检测到是 CORS 请求, 然后添加一个origin
字段(其中包含页面源信息: 协议、域名、端口) => 服务端收到后作相应的处理(对比origin
, 服务端判断这个源是否接受)返回结果给浏览器 => 浏览器检查响应头是否允许跨域信息 => 允许, 那就当做没事发生. 不允许, 浏览器抛出相应的错误信息(值得一提的是, 这时状态码也还有可能是200).
非简单请求执行顺序又有些不同. 在发生 CORS 请求时, 浏览器预先发送一个option
请求. 浏览器这种行为被称之为预检请求(Preflighted request). 其中包含如下的请求头:
- origin: 同上,包含页面源信息.
- Access-Control-Request-Methods: 请求方法
- Access-Control-Request-Header: 自定义头部信息, 多个头部以逗号分隔(可选, 看请求时有没有定义请求头)
举个栗子, 我们用JQuery发送一段JSON格式的请求做演示:
1 | var result; |
这时请求头(Request Headers)信息如下:
1 | Accept: */* |
服务端接收到预检请求后, 判断是否允许这种类型的请求. 在响应头(Response Header)上返回如下头部与浏览器进行交流:
- Access-Control-Allow-Origin: 服务端允许的源信息
- Access-Control-Allow-Methods: 服务端允许的方法, 多个方法可以使用顿号分隔
- Access-Control-Allow-Headrs: 服务端允许的头部, 多个头部可以使用顿号分隔
当预检请求被通过后, 我们原本想要发送的请求才会发送出去.
另外, 细心的你或许已经注意到了, 非简单请求这一来一回需要发送两次请求, 如果频率高的情况下岂不是很费性能又影响效率? 所幸的是HTTP协议新增(IE10+)了一个响应头用于缓存预检请求. 服务端在响应头添加如下字段:
1 | Access-Control-Max-Age: 3600 |
这个响应头表示这个预检请求可以缓存多长时间, 单位为秒. 这里3600s = 1h, 也就是说一个小时内可以不用再发预检命令了.
带 cookie 的跨域请求
默认情况下, 跨域请求是不带上 cookie 的. 前端需要将withCredentials
属性设置为true
, 同时还需要服务端设置Access-Control-Allow-Credentials
为true
启动 cookie. 如果在发送 cookie 的时候, 浏览器检测到服务端响应头没有这个头部, 那么就会在控制台抛出一个错误.
另外, 还有一个值得注意的是. 服务端响应头设置了Access-Control-Allow-Origin: *
的话, 是不能满足带 cookie 的跨域请求的. 因此有这种场景不能使用通配符, 需要全匹配字段.
CORS 总结
简单总结一下 CORS. CORS的出现也是为了解决跨域的问题. 只不过和JSONP
不同, 它是纳入规范的一部分, 它几乎支持所有的类型的HTTP请求(JSONP只能使用GET). 唯一美中不足的也就是兼容性的问题, 因此可以使用JSONP作向下的兼容
事实上前端在 CORS 上并没有多少可操作的余地, 主要的还是浏览器来处理、服务端在设置, 但是并不代表我们就不需要了解这些知识啦.
Nginx
Nginx
是一个高性能的 HTTP 和反向代理服务器。通过配置 Nginx, 我们可以实现跨域请求的代理转发。它可以将前端请求转发到不同的后端服务器,从而绕过浏览器的同源策略限制。
以下是一个配置 Nginx 反向代理的实例:
配置反向代理
假设我们有个前端应用,前端域名为 frontend.example.com
,请求后端 API 服务器的地址为:https://api.example.com
。我们在 Nginx
中配置一个反向代理,将前端的请求转发到后端服务器。编辑 Nginx
的配置文件(例如:/etc/nginx/nginx.conf 或 /etc/nginx/conf.d/default.conf):
1 | http { |
这里,我们将所有 /api/
开头的请求代理到 https://api.example.com
。
设置跨域响应头
为了让浏览器允许跨域请求,我们需要在 Nginx 配置文件中设置响应头。继续编辑配置文件,在 location /api/
部分添加如下内容:
1 | location /api/ { |
这里我们设置了跨域请求所需的响应头。
Access-Control-Allow-Origin
允许指定来源的请求Access-Control-Allow-Methods
允许哪些 HTTP 方法Access-Control-Allow-Headers
允许哪些请求头。当请求方法为OPTIONS
时,我们直接返回204
状态码,表示预检请求成功。
重启 Nginx
配置完后记得重启 Nginx
1 | sudo nginx -t # 检查配置文件语法是否正确 |
现在就可以通过代理的方式来请求了。
代理服务器
本地开发一般有像 webpack、parcel 之类的脚手架,脚手架内一般都可以启动一个本地服务器。这个本地服务器是通过 Node.js 启动的,它可以将给定请求代理为本地的请求,从而绕开浏览器的跨域限制。
以常用的 webpack
为例:
webpack
使用 webpack-dev-server
实现代理请求、热更新、静态资源服务等功能。代理请求功能是指将特定的请求代理到其他服务器。通过将请求代理到后端服务器,开发人员可以绕过浏览器的同源策略限制。
以下是实现使用步骤:
配置 webpack config
在 Webpack
配置文件(默认为 webpack.config.js)中,我们可以设置一个代理规则,将特定的请求代理到其他服务器。这里是一个简单的例子:
1 | module.exports = { |
在这个例子中,我们将所有 /api
开头的请求代理到 http://api.example.com
。changeOrigin
选项将更改请求头中的主机名以匹配目标服务器,pathRewrite
选项用于重写请求路径,删除了请求路径中的 /api
。
启动开发服务器
1 | npx webpack serve |
当浏览器向 webpack-dev-server
发送请求时,它会根据配置文件中的代理规则将请求转发到目标服务器。由于这些请求实际上是从开发服务器发出的,而不是直接从浏览器发出的,因此可以绕过浏览器的同源策略限制。