CORS过程的介绍以及跨域问题
参考文章:https://mp.weixin.qq.com/s/Re1fvKKzi-rPpu6SmpqTJA
文中的动图以及部分总结语句均引用与参考文章。我这里只是为了让自己理解深刻一点而尝试用自己的语言去总结一遍。推荐阅读原文
说到跨域问题,我们最常见的就是在我们向服务器发起请求的时候会遇到。比如说如下情况
假设我们正在访问 https://api.mysebsite.com
的站点,当我们点击按钮,向 https://api.mysebsite/users.com
发送请求,获取网站上的一些用户信息。这个时候从结果上是完美的。原因是,根据同源策略,我们发起请求的域名,端口,协议均是一至的。浏览器并不会产生跨域报错。
而如果,以上提到的三点其中一点不满足的站点,再向服务器发起请求,那么这个时候就会触发跨域报错。如下图
而以上两种情况出现的原因,其实就是我们今天要介绍的内容。
首先我们先来介绍一下同源策略
同源策略
浏览器网络请求的时候,有一个同源策略的机制,即默认情况下,使用API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源。也就是我们上面提到的,必须要求域名,端口,协议都要一至。请求才能发送成功。而只要其中一点不一致,那么该请求也算是跨域的。
所以我们同样可以看得出来。同源策略会限制以下三种行为:
- Cookie,LocalStorage,和IndexDB 访问受限
- 无法操作跨域DOM(常见的如 iframe)
- js发起的XHR和Fetch请求受限
作用
说了这么多其实,我们可以看出同源策略的作用其实就是为了保证用户的信息安全。打个比方,如果没有同源策略,那么当你在不小心的情况下,点击了网页的钓鱼网站。然后恶意的网站很容易就能利用重定向把你带到一个iframe 的 攻击网站,这个iframe会自动加载银行网站,并通过Cookie登录你的账号。然后操作你的DOM,进行一系列的危险操作。
而我们当然不希望这种情况发生,这个时候同源策略就起到有效的保护作用。因为它确保了我们只能访问同一站点的资源
好了,接下来要说的就是CORS了。
浏览器的CORS
浏览器出于安全的考虑,会限制从脚本内发起的跨域HTTP请求。(注意是脚本内)例如XHR 和 Fetch 就遵循同源策略。这意味着使用 API 的Web应用程序只能从加载应用程序的同一个域请求HTTP资源。
在日常的开发中,我们很多时候都会跨域去请求别的站点的资源。而这个时候我们为了解决跨域的问题就要利用CORS机制。
CORS(Cross-Origin Resource Sharing),即跨域资源共享。如字面意思,CORS机制的存在就是为了让我们在保证安全的前提下,实现访问不同域下的资源,这算是放宽的政策。
浏览器可以利用CORS机制,放行一些符合规范的跨域访问,阻止不符合规范的跨域访问。下面我我们就来介绍一下浏览器内部是如何实现的。
Web程序发出跨域请求后,浏览器会自动向我们的HTTP header添加一个额外的字段 Origin
。Origin
标记了请求的站点来源:
GET https://api.website.com/users HTTP/1/1
Origin: https://www.mywebsite.com -> 浏览器自己携带的
为了使浏览器允许访问跨域资源,服务器返回的 response 还需要加一些响应头字段,这些字段可以显式的表明此服务器是否允许跨域请求。
服务端的CORS
作为服务端人员,我们为了允许符合规则的跨域请求。我们可以通过在HTTP的响应中添加响应字段 Access-Control-*
来表明是否允许跨域请求。根据这些CORS响应头字段,浏览器可以允许一些被同源策略限制的跨域响应
虽然有好几个CORS的字段,但是有一个必须要加的字段是Acess-Control-Allow
。这个头字段的值指定了哪些站点被允许跨域访问资源的。
- 如果我们有服务器的开发权限,我们可以给
https://www.mywebsite.com
加上访问权限,只需要把这个域名添加到Access-Control-Allow-Origin
中
现在这个字段会被添加到服务端的响应报文中,然后返回给客户端。然后这个时候客户端再向服务端发起跨域请求,同源策略将不会再限制 https://api.mywebsite.com
站点返回的资源。
报文如下:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mywebsite.com
Date: Fri, 11 Oct 2019 15:47 GM
Content-Length: 29
Content-Type: application/json
Server: Apache
{user: [{...}]}
- 收到服务器返回的response后,浏览器中的CORS机制会检查
Access-Control-Allow-Origin
的值是否等于 request 中 Origin的值
- 浏览器通过校验之后,前端成功接收到跨域站点的资源
而相反,如果对比Access-Control-Allow-Origin
和 Origin
的值不一致的时候,浏览器会抛出一个 CORS Error的报错信息。
当然,我们还可以通过配置 * 作为
Access-Control-Allow-Origin
的值,这样的话任意的外域访问都会被允许。但是这并不是解决允许多域名请求同一站点这个场景的最优解下面我会再单独讲一讲这种场景的解决方案。
除了 Access-Control-Allow-Origin
字段头之外,开发人员还可以通过其他字段对请求作出限制。比如说 Access-Control-Allow-Methods
该字段用来指明跨域请求所允许的 HTTP 方法。
如上图中,只有请求方法为 GET
,POST
或 PUT
方法被允许跨域访问资源。其他的HTTP方法,例如 PATCH
和 DELETE
都会产生预检。
这里就引申出一些别的内容,比如说预检
什么是预检呢?
预检 = 预请求(options请求)
- 在发起实际请求之前,如果这个请求是一个复杂请求,那么客户端会先发出一个options请求。预请求中的
Access-Control-Request-*
包含了我们将要处理的实际请求信息。
首部字段
Access-Control-Request-Method
会告知服务器,实际请求要用到什么方法
首部字段Access-Control-Request-Headers
会告知服务器,实际请求将附带的自定义请求首部字段是什么
OPTIONS https://api.mywebsite.com/user/1 HTTP/1.1
Origin: https://www.mywebsite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type
- 服务器接收到预请求后,会返回一个没有 body的HTTP响应,这个响应标记了服务器允许的 HTTP 方法和 Http Header字段
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.mywebsite.com
Access-Control-Request-Method: GET POST PUT
Access-Control-Request-Headers: Content-Type
- 浏览器收到预请求的响应,并检查是否应允许发送实际请求
上图漏了
Access-Control-Allow-Headers: Content-Type
- 如果预请求通过了,浏览器会将实际请求发送到服务器,然后服务器会返回我们想要的资源
而此处有一个优化点,既然每一次复杂请求都会重复 options请求,那么我们可以通过设置一个字段
Access-Control-Max-Age
来缓存预请求的响应。浏览器可以使用缓存来代替发送新的预请求。
到这里,CORS和跨域的关系基本就说清楚了。下面我们再说一点补充
- 认证
XHR 或 Fetch 与 CORS的一个有趣的特性是,我们可以基于 Cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XHR 或 Fetch 请求,浏览器不会发送身份凭证信息。
尽管 CORS 默认情况下不发送身份凭证,但我们仍然可以通过添加Access-Control-Allow-Credentials
CORS响应头来更改它
如果要在跨域请求中包含 cookie 和其他授权信息,我们需要做一下操作:
- XHR 请求中将
withCredentials
字段设置为 true - Fetch 请求中将
credentials
字段设置为 include - 服务器把
Access-Control-Allow-Credentials: true
添加到响应头中
// 浏览器 fetch 请求
fetch('https://api.mywebsite,com.users', {
credentials: "include"
})
// 浏览器 XHR 请求
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// 服务器添加认证字段
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
做到了上面几点之后,就能在跨域请求中携带身份凭证了。
补充
- 我们上面提到可以通过设置
Access-Control-Allow-Origin
去允许跨域请求。但是如果想要允许多个域名访问同一个站点的资源的时候。我们显然是不能写死的。这个时候有就以下几个解决方案:
a. Access-Control-Allow-Origin 设置为 * (不推荐)
b. 通过nginx代理服务器进行设置
c. 修改 api 接口,在每个api上添加响应头 (待考究)
d. 拦截器方式 (微软提供的最佳方案,实际上是对c方案的封装)d的方案就是写一个拦截器,应用到所有控制器上,在拦截器里控制来访域名,动态设置Access-Control-Allow-Origin的值.
简单请求不会触发预请求,但是会根据请求响应的报文去判断是否跨域了(即对比origin,和Access-Control-Allow-Origin的值是否一致)
简单请求这种,实际是已经发去了的请求,也就是说请求已经到了服务器了,然后返回来了。只是浏览器拦截了响应,而并不是拦截了请求本身
以上就是全部内容了