浏览器跨域 CORS 协议基础知识梳理
最近老是遇到联调的时候出现跨域问题,便萌生把跨域问题理理清楚的想法
首先罗列一些基础的知识点
先贴一下官方文档
CORS 是什么
随着Web开放的程度越来越高,通过浏览器跨域获取资源的需求已经变得非常普遍。在我看来,如果Web API不能针对浏览器提供跨域资源共享的能力,它甚至就不应该被称为Web API。从另一方面来看,浏览器作为进入Internet最大的入口,是各大IT公司的必争之地,所以浏览器市场出现了种类繁多、鱼龙混杂的局面。针对这两点,我们迫切需要一种能够被各个 浏览器厂商 共同遵循的标准来对跨域资源共享作出规范,这就是由W3C指定2的CORS(Cross-Origin Resource Sharing)规范。
划重点 : 这里说的是浏览器厂商,下面我们会提到
了解 CORS 实现原理之前,先了解两个基础概念
在 CORS 请求返回头中,我们会用到的
ResponseHeader
名称 描述 Access-Control-Allow-Origin 允许跨域的域名 Access-Control-Allow-Methods 允许跨域的http method Access-Control-Allow-Headers 允许携带的 request header Access-Control-Allow-Credentials 是否支持用户凭证 Access-Control-Max-Age 失效时间 Access-Control-Expose-Headers ResponseHeader中允许暴露给JavaScript的header CORS 的 简单请求 和 复杂请求
W3C的CORS规范将跨域资源请求划分为两种类型,一种被称为
简单请求(Simple Request)
。要弄清楚CORS规范将那些类型的跨域资源请求划分为简单请求的范畴,需要额外了解几个名称的含义,其中包括简单(HTTP)方法(Simple Method)
、简单(请求)报头(Simple Header)
和自定义请求报头(Author Request Header/Custom Request Header)
。什么是简单请求
简单请求必须满足以下条件限制
请求的方法
METHOD
只能是以下类型之一HEAD
GET
POST
CORS规范将
GET
、HEAD
和POST
这三个HTTP方法视为简单HTTP方法(Simple Method)
请求报文中的头信息
RequestHead
不能超出如下限制
CORS规范将服务如下条件的跨域资源请求划分为简单请求:
请求采用简单HTTP方法,并且其自定义请求报头空或者所有自定义请求报头均为简单请求报头
。之所以作如此划分是因为具有这些特性的请求不是以更新(添加、修改和删除)资源为目的,服务端对请求的处理不会导致自身维护资源的改变。什么是复杂请求
不是简单请求的请求称之为复杂请求
CORS 的两个步骤 取得授权
和 获取资源
CROS利用资源提供者的显式授权
来决定目标资源是否应该与消费者共享。也就是说,浏览器需要取得资源提供者的授权
之后才会将其提供的资源分发
给消费者。那么,资源的提供者如何进行资源的授权,并将授权的结果告诉浏览器呢?
具体的实现其实很简单。CORS将资源的获取分为两个步骤 : 取得授权
和 获取资源
取得授权
预检查Preflight
按照CORS规范的规定,浏览器应该采用一种被称为
预检(Preflight)
的机制来完成非简单跨域资源请求。
浏览器会发送一个如下的请求给资源提供者1
2
3
4
5
6
7
8
9
10
11OPTIONS /simple-request HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
Origin: http://localhost:8000
User-Agent: Chrome/98
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7其中有三个字段有明确的含义
Origin
代表申请授权(申请跨域)的来源,即请求页面所在的站点Access-Control-Request-Method
申请跨域请求采用的HTTP METHOD
Access-Control-Request-Headers
申请跨域请求携带的自定义报头列表
浏览器会根据资源提供者返回的头
(ResponseHeader)
中的字段来判断是否获得跨域授权:Access-Control-Allow-Origin
表明当前资源可以跨域的站点,
如果包含当前请求的Origin
时,则代表当前站点可以使用此跨域资源。
如果返回Access-Control-Allow-Origin: *
则代表此资源是一个公共资源,所有站点都可以使用。Access-Control-Allow-Methods
表明当前资源可以跨域使用的HTTP METHOD
如果包含当前预检的Access-Control-Request-Method
值,则代表当前站点可以使用此HttpMethod
跨域获取资源
如果返回Access-Control-Allow-Methods: *
则代表所有HttpMethod
都可以。Access-Control-Allow-Headers
表明当前资源可以跨域使用的自定义报头列表
,
如果包含当前预检的Access-Control-Request-Headers
值,则代表当前站点可以使用这些自定义报头列表
跨域获取资源
预检请求
并不是返回成功(httpCode:200)
就代表通过了,浏览器会根据返回的(ResponseHeader)
判断是否能获得授权,如果有上面的三个条件有任何一个没有满足,都会被浏览器视为取得授权
,抛出我们常见的跨域错误。比如:
- No
Access-Control-Allow-Origin
header is present on the requested resource
这个就是应为没有返回Access-Control-Allow-Origin
这个header
- Request header field
content-type
is not allowed by Access-Control-Allow-Headers in preflight response
这个就是Access-Control-Allow-Headers
没返回或者没有包含content-type
- Method
OPTIONS
is not allowed by Access-Control-Allow-Methods in preflight response.
这个就是因为Access-Control-Allow-Methods
没返回或者没有包含OPTIONS
这些都是预检返回
200
,浏览器判断授权失败
,抛出的跨域错误获取资源
当预检返回被浏览器验证通过后
(取得授权)
,才会向发送普通请求(非跨域请求)
一样,发送请求
模拟
假设我们的浏览器地址为
http://localhost:8000
,
后台服务为http://localhost:3000
如果我们要请求 PUT /simple-request-custom-content-type
这个接口,那么浏览器会先发送的 预检
大概是下面这样的:
1
2
3
4
5
6
7
8
9
10
11OPTIONS /simple-request-custom-content-type HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Origin: http://localhost:8000
User-Agent: Chrome/98.0.4758.80
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
要想预检通过,返回的ResponseHeader
大概是这样的
1
2
3
4
5
6
7
8HTTP/1.1 200 OK
Content-Type: Application/json
Access-Control-Allow-Origin: http://localhost:8000 // 允许 http://localhost:8000 访问
Access-Control-Allow-Headers: content-type // 允许使用 content-type 这个header
Access-Control-Request-Method: PUT // 允许使用 PUT 方法
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
这个时候浏览器就会判定为(取得授权)
,继续请求了。
简单请求的不同之处
对于简单跨域资源请求来说,浏览器将两个步骤(取得授权
和获取资源
)合二为一,由于不涉及到资源的改变,所以不会带来任何副作用(Side Effect
)。
模拟
假设我们的浏览器地址为
http://localhost:8000
,
后台服务为http://localhost:3000
如果我们要请求GET /simple-request
这个接口,那么浏览器会发送的请求大概是下面这样的:
1
2
3
4
5
6
7
8
9GET /simple-request HTTP/1.1
Host: localhost:3000
Connection: keep-alive
User-Agent: Chrome
Accept: */*
Origin: http://localhost:8000
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
这时候要想跨域通过,返回的ResponseHeader
应该是这样的
1
2
3
4
5
6
7HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8000 // 允许 http://localhost:8000 访问
Access-Control-Request-Method: GET // 允许使用 PUT 方法
Date: Thu, 24 Feb 2022 14:25:46 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
并一起返回我们的数据ResponseBody
,但是,如果返回的ResponseHeader
不合法,即不能判定为取得授权
浏览器就会抛出跨域错误了
,并且我们在Network
中也是看不到返回的结果的。
有一些后端知识的小伙伴应该知道,其实这时候后端的接口已经完成调用了,只是浏览器没有展示而已
这也是很多后端小伙伴会困惑的问题:为什么我都返回了,还跨域。我的Postman
都能调通。我真不知道是什么原因!!!
再看两个案例
假设我们的浏览器地址为
http://localhost:8000
,
后台服务为http://localhost:3000
简单请求
Simple Method
1
2
3
4
5
6
7
8
9GET /simple-request HTTP/1.1
Host: localhost:3000
Connection: keep-alive
User-Agent: Chrome/98.0.4758.80
Accept: */*
Origin: http://localhost:8000
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9只要返回头里包含
Access-Control-Allow-Origin: http://localhost:8000
就可以了这是一个简单请求
Simple Method
+Simple Header
1
2
3
4
5
6
7
8
9
10GET /simple-request HTTP/1.1
Host: localhost:3000
Connection: keep-alive
User-Agent: Chrome/98.0.4758.80
Content-Type: multipart/form-data
Accept: */*
Origin: http://localhost:8000
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9只要返回头里包含
Access-Control-Allow-Origin: http://localhost:8000
就可以了Simple Method
+Custom Header
1
2
3
4
5
6
7
8
9
10GET /unstandard-content-type HTTP/1.1
Host: localhost:3000
Connection: keep-alive
User-Agent: Chrome/98.0.4758.80
Content-Type: application/json
Accept: */*
Origin: http://localhost:8000
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9由于
Content-Type
为application/json
,这不是一个简单请求。会触发预检
。
预检要想通过,ResponseHead
中要包含1
2Access-Control-Allow-Origin: http://localhost:8000 // 允许 http://localhost:8000 访问
Access-Control-Request-Headers: Content-Type // 允许使用 Content-Type 头
到这里,大致的 CORS 流程就完了,还有几个小的不常用的知识点,再补充下
Access-Control-Max-Age
如果每次都预检Preflight
的话,那不是跨域请求会增加服务器负担呢?这就是Access-Control-Max-Age
的作用了
预检响应结果会被浏览器缓存,在
Access-Control-Max-Age
报头设定的时间内,缓存的结果将被浏览器用户进行授权检验,所以在此期间不会再有预检请求发送。
Access-Control-Allow-Credentials
在默认情况下,利用XMLHttpReuqest
发送的Ajax
请求不会携带用户凭证相关的敏感信息,这里的用户凭证类型包括Cookie
、HTTP-Authentication
报头等。如果需要用户凭证附加到Ajax
请求上,需要将XMLHttpReuqest的withCredentials
属性设置为True
。
对于CORS
来说,是否支持用户凭证也是授权检验的一个环节。换句话说,只有在服务端显式声明支持用户凭证的情况下,携带了用户凭证的请求才会被认为是有效的。在W3C的CORS规范来说,服务端利用响应报头Access-Control-Allow-Credentials
来表明自身是否支持用户凭证。
Access-Control-Expose-Headers
资源提供者可以通过设置Access-Control-Expose-Headers
这个报头对响应报头进行授权。
具体来说,如果我们后端返回的ResponseHeader
有一些自定义的Custom Header
1 | debugId: 123 |
但是我们却在Network
中无法看到这些Header
,而在Postman
或curl
中却可以看到,那是因为浏览器屏蔽了这些header
,需要添加如下header
1 | Access-Control-Expose-Headers: debugId,abPlan |
总结
跨域问题并不是说接口没有异常就可以了,很多时候是因为返回的
ResponseHeader
不能通过浏览器预检
关于CORS
大概的也就讲完了,我只是一个前端小菜鸡,若有不正之处,还请多多指教
最后,放一个demo链接
划重点
跨域问题,千万不要被 : “Postman
或curl
能调通” 这句话误导啊,这不是接口掉不通的问题,是接口返回头不符合规范的问题啊!!!
参考文章
[CORS:跨域资源共享] W3C的CORS Specification
W3C指定的CORS(Cross-Origin Resource Sharing)规范介绍