CORS(https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-
CN%2Fdocs%2FGlossary%2FCORS) 是一个W3C标准,全称是 跨域资源共享 (Cross-Origin Resource
Sharing),也有译为“跨源资源共享”的。
它允许浏览器向跨源服务器,发出
XMLHttpRequest(https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-
CN%2Fdocs%2FWeb%2FAPI%2FXMLHttpRequest)(XHR) 或 [Fetch
API](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-
CN%2Fdocs%2FWeb%2FAPI%2FFetch_API) 跨域 HTTP
请求,从而克服了同源(https://links.jianshu.com/go?to=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2016%2F04%2Fsame-
origin-policy.html)使用的限制。
本文内容主要参考于跨域资源共享 CORS 详解和 MDN 相关文档。
CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源。
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现
AJAX 请求跨源,就会自动添加一些附件的头信息,有时还会多处一次附件的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口(响应报文包括了正确的 CORS 响应头),就可以跨源通信。
同源策略是一个重要的安全策略,它用于限制一个 Origin 的文档或它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
**如果两个 URL
的协议(https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-
CN%2Fdocs%2FGlossary%2FProtocol)(Protocol)、主机(https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-
CN%2Fdocs%2FGlossary%2FHost)(Host)、端口(https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-
US%2Fdocs%2FGlossary%2FPort)(Port,如果有指定的话)都相同的话,那么这两个 URL 是同源的,否则是不同源的。**
下表给出了与 URL http://store.company.com/dir/page.html
的源进行对比的示例:
URL | 结果 | 原因 |
---|
http://store.company.com/dir2/other.html
| 同源 | 只有路径不同。
http://store.company.com/dir/inner/another.html
| 同源 | 只有路径不同。
https://store.company.com/secure.html
| 不同源 | 协议不同。
http://store.company.com:81/dir/etc.html
| 不同源 | 端口不同(http:// 默认端口是 80)。
http://news.company.com/dir/other.html
| 不同源 | 主机不同。
关于 IE 浏览器的特例,其中差异点是不规范的,其他浏览器未做出支持。
出于安全性,浏览器限制脚本内发起的跨域 HTTP 请求,例如常见的 XHR、Fetch API 都遵循同源策略。
浏览器将 CORS 请求分成两类: 简单请求 (simple request)和非简单请求(not-so-simple request)。
若满足下述所有条件,则该请求可视为“简单请求”:
请求方法是以下三种之一:
HEAD
GET
POST
HTTP 的头信息不超出以下几种字段:Accept
Accept-Language
Content-Language
Content-Type
DPR
Downlink
Save-Data
Viewport-Width
Width
Content-Type 的值仅限于以下三种之一text/plain
multipart/form-data
application/x-www-form-urlencoded
请求中的任意XMLHttpRequestUpload
对象均没有注册任何事件监听器。 请求中没有使用ReadableStream
对象。
凡是不同时满足以上条件,均属于非简单请求。
注意: 这些跨站点请求与浏览器发出的其他跨站点请求并无二致。如果服务器未返回正确的响应首部,则请求方不会收到任何数据。因此,那些不允许跨站点请求的网站无需为这一新的 HTTP 访问控制特性担心。
一个简单的 XHR 请求示例:
客户端(Client):
// Client 客户端(http://192.168.1.105:8080)
function xhrRequest() {
// 创建 xhr 对象
const xhr = new XMLHttpRequest()
// 通过 onreadystatechange 事件捕获请求状态的变化
// 必须在 open 之前指定该事件,否则无法接收 readyState 0 和 1 的状态
xhr.onreadystatechange = () => {
console.log(xhr.status)
console.log(xhr.readyState)
}
// 捕获错误
xhr.onerror = err => {
console.log('error:', err)
}
// 初始化请求
xhr.open('GET', 'http://192.168.1.105:7701/config', true)
// 发送请求
xhr.send(null)
}
xhrRequest()
服务端(Server):
// Server 服务端(通过 node 命令即可启动,http://192.168.1.105:7701)
const http = require('http')
const port = 7701
const allowedOrigin = 'http://192.168.1.105:8080' // 这个是我本地客户端的 Origin(源)
const server = http.createServer((request, response) => {
if (request.headers.origin === allowedOrigin) {
response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
}
if (request.url === '/config') {
response.end(JSON.stringify({ name: 'Frankie', age: 20 }))
}
})
server.listen(port)
发起 XHR HTTP 请求,我们可以在控制台看到请求报文和响应报文。(View source)
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就在头信息之中,增加一个 Origin
字段。
通过以上示例发起 HTTP GET
请求,请求报文如下:
GET /config HTTP/1.1
Host: 192.168.1.105:7701
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
Accept: */*
Origin: http://192.168.1.105:8080
Referer: http://192.168.1.105:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
上面的头信息中,Origin
字段用来说明,本次请求来自哪个源(协议 + 主机 + 端口)。服务器根据这个值,绝对是否同意这次请求。
如果 Origin
指定的源,不在许可范围内,服务器会返回一个正确的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 `Access-
Control-Allow-Origin字段(相见下文),就知道出错了,从而抛出一个错误,被
XMLHttpRequest的
onerror`
回调函数捕获。注意,这种错误无法通过状态识别,因为 HTTP 回应的状态码有可能是 200
。
在上述服务端示例中,我们指定的源就是
http://192.168.1.105:8080
,因此可以正常发起请求,并拿到接口响应数据:{"name":"Frankie","age":20}
。
假设,我们将服务端示例许可的 Origin
去掉:
// if (request.headers.origin === allowedOrigin) {
// response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
// }
我们再次发起 /config
请求的时候,可以看到控制台抛出跨域错误了:
需要注意的是,尽管无论从控制台看到的报错、还是 Network 选项卡的 Failed to load response data,都可以知道我们的请求失败了。但......其实是这样的: 就本文服务端示例而言,即使是跨域请求,也是会返回响应数据的,只是 JavaScript 脚本获取到的结果是“失败”而已。
那不对啊,如果服务器正常返回数据,而前端却失败了,那信息不对称啊,中间商赚差价了?怎么回事呢?原因是浏览器在中间搞鬼,那浏览器在中间扮演了什么角色呢?
当发起 CORS 请求时,浏览器首先会在请求报文上自动加上
Origin
的字段(它的值由当前页面的 Protocol + Host + Port 部分组成),到达服务端之后,会做出相应的处理并返回数据。由于服务端并没有给响应报文的头部设置Access-Control-Allow- Origin
(前面说把这块注释掉了),自然浏览器接收到的响应报文中就不含Access-Control-Allow- Origin
,当浏览器就判断到请求报文与响应报文的Origin
不相等,此时它不会将服务器的响应数据 JavaScript 脚本,即我们的 XHR 对象无法得到服务器的响应数据,并且会触发 XHR 对象的onerror
事件,让脚本来捕获错误。所以,我们就看到请求失败了。(这种情况下,可以通过抓包工具查看服务器返回的数据)还有,只有在
Origin
指定的源在许可范围内,服务器响应报文才会多出这些Access-Control-Allow- Origin
、Access-Control-Allow-Credentials
、Access-Control-Expose-Headers
头信息字段。(可从客户端 Network 选项卡观察到)
以下与 CORS 请求相关的头信息字段,都以 Access-Control-
开头。
Access-Control-Allow-Origin: http://10.16.4.226:8080
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Date
这个字段是必须的。它的值要么是请求时 Origin
字段的值,要么是一个 *
,表示接受任意的源(域名)的请求。
这个字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为
true
,即表示服务器明确许可,Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true
,如果服务器不要浏览器发送
Cookie,删除该字段即可。
// Client
xhr.withCredentials = true
// Server
response.setHeader('Access-Control-Allow-Credentials', true)
当客户端携带 Cookie 发起请求,同时服务端允许携带 Cookie 的情况下,可以看到请求报文包含了 Cookie 信息:
Cookie: username=Frankie
这个字段可选。CORS 请求时,XMLHttpRequest
对象的 getResponseHeader()
方法只能拿到 6 个基本字段:
Cache-Control
、Content-Language
、Content-Type
、Expires
、`Last-
Modified、
Pragma。如果想要拿到其他字段,就必须在
Access-Control-Expose-Headers` 里面指定。(但由于
W3C 的限制,不指定的情况下,客户端获取到的值可能为 null
)
例如,服务端将 Access-Control-Expose-Headers
指定为 `"Date,Access-Control-Allow-
Origin"`,我们可以通过 XMLHttpRequest 对象的 getResponseHeader() 方法获取对应字段的值。
// Client
xhr.onreadystatechange = () => {
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.getResponseHeader('Cache-Control')) // null
console.log(xhr.getResponseHeader('Content-Language')) // null
console.log(xhr.getResponseHeader('Content-Type')) // null
console.log(xhr.getResponseHeader('Expires')) // null
console.log(xhr.getResponseHeader('Last-Modified')) // null
console.log(xhr.getResponseHeader('Pragma')) // null
console.log(xhr.getResponseHeader('Date')) // "Sun, 06 Jun 2021 14:34:34 GMT"
console.log(xhr.getResponseHeader('Access-Control-Allow-Origin')) // "http://192.168.1.105:8080"
console.log(xhr.getResponseHeader('X-Custom-Header')) // Error: Refused to get unsafe header "X-Custom-Header"
}
}
// Server
if (request.headers.origin === allowedOrigin) {
response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
response.setHeader('Access-Control-Allow-Credentials', true)
response.setHeader('Access-Control-Expose-Headers', 'Date,Access-Control-Allow-Origin')
}
如果连接未完成,响应中不存在报文项,或者被 W3C 限制,则返回
null
。假设如上示例,自定义头信息X-Custom-Header
字段,且服务端未对其进行设置,则会抛出错误:Refused to get unsafe header "X-Custom-Header"
上面说到,CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发送到服务器,一方面要服务器同意,指定 `Access-
Control-Allow-Credentials` 字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在 AJAX 请求中打开 withCredentials
属性。
xhr.withCredentials = true
否则,即使服务器同意发送 Cookie,浏览器也不会发送。或者服务器要求设置 Cookie,浏览器也不会处理。
但是,如果省略 withCredentials
设置,有的浏览器还是会一起发送 Cookie。这时,可以显示关闭 withCredentials
。
xhr.withCredentials = false
需要注意的是,如果要发送 Cookie,Access-Control-Allow-Origin
就不能设为
*
(否则会抛出如下错误),必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源策略,只有用服务器域名设置的 Cookie
才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie
也无法读取服务器域名下的 Cookie。
调整一下示例:
// Client 客户端
function xhrRequest() {
// 创建 xhr 对象
const xhr = new XMLHttpRequest()
// 通过 onreadystatechange 事件捕获请求状态的变化
xhr.onreadystatechange = () => {
console.log(xhr.status)
console.log(xhr.readyState)
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.getResponseHeader('Date'))
console.log(xhr.getResponseHeader('Access-Control-Allow-Origin'))
}
}
// 捕获错误
xhr.onerror = err => {
console.log('error:', err)
}
// 初始化请求
xhr.open('PUT', 'http://192.168.1.105:7701/config', true)
// 设置自定义头部(必须在 open 之后)
xhr.setRequestHeader('X-Custom-Header', 'foo')
// 发送请求
xhr.send(null)
}
xhrRequest()
// Server 服务端
const http = require('http')
const port = 7701
const allowedOrigin = 'http://192.168.1.105:8080' // 这个是我本地客户端的 Origin(源)
const server = http.createServer((request, response) => {
if (request.headers.origin === allowedOrigin) {
response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
response.setHeader('Access-Control-Allow-Credentials', true)
response.setHeader('Access-Control-Allow-Methods', 'PUT')
response.setHeader('Access-Control-Allow-Headers', 'X-Custom-Header')
response.setHeader('Access-Control-Expose-Headers', 'Date,Access-Control-Allow-Origin')
}
if (request.url === '/config') {
response.end(JSON.stringify({ name: 'Frankie', age: 20 }))
}
})
server.listen(port)
非简单请求时那种对服务器有特殊要求的请求,比如请求方法是 PUT
或 DELETE
,或者 Content-Type
字段的类型是
application/json
。
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求 (preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的
XMLHttpRequest 请求,否则就报错。
上面代码中,HTTP 请求的方法是 PUT
,并且发送一个自定义头信息 X-Custom-Header
。
浏览器发现,这是一个非简单请求,就自动发出一个“预检请求”,要求服务器确认可以这样请求。下面是这个“预检请求”的 HTTP 头信息。
OPTIONS /config HTTP/1.1
Host: 192.168.1.105:7701
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: x-custom-header
Origin: http://192.168.1.105:8080
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
Sec-Fetch-Mode: cors
Referer: http://192.168.1.105:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
“预检请求”用的请求方法是 OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字是 Origin
,表示请求来自哪个源。
除了 Origin
字段,“预检请求”的头信息包括两个特殊字段。
,
分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上面示例是 X-Custom-Header
。Tips:预检请求可以在调试器 Network 选项卡中查看,如下图:
服务器收到“预检请求”以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-
Request-Headers 字段以后,确认允许跨域请求,就可以做出回应。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://192.168.1.105:8080
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Expose-Headers: Date,Access-Control-Allow-Origin
Date: Sun, 06 Jun 2021 15:27:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 27
上面的 HTTP 回应中,关键的是 Access-Control-Allow-Origin
字段,表示
http://192.168.1.105:8080
可以请求数据。该字段也可以设为 *
,表示同意任意的跨源请求。(本示例携带了 Cookie
信息,不可设为 *
)
Access-Control-Allow-Origin: *
如果服务器否定了“预检请求”,会返回一个正常的 HTTP 回应,但是没有任何的 CORS
相关的头信息字段。这是,浏览器就认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest
对象的 onerror
回调函数捕获。
如果将服务端的 Access-Control-Allow-Headers
注释掉:
// response.setHeader('Access-Control-Allow-Headers', 'X-Custom-Header')
控制台会打印出如下的报错信息:
服务器回应的其他 CORS 相关字段如下:
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
,
分隔的一个字符串,表明服务器支持的所有跨域请求。注意,返回的是 所有
支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次“预检请求”。(本示例仅设置了一个 PUT
方法。)Access-Control-Request-Headers
字段,则 Access-Control-Allow-Headers
字段是必须的,它也是一个逗号 ,
分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检请求”的字段。一旦服务器通过了“预检请求”,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin
头信息字段。服务器的回应,也都会有一个
Access-Control-Allow-Origin
头信息字段。
下面是“预检请求”之后,浏览器的正常 CORS 请求的请求报文。
PUT /config HTTP/1.1
Host: 192.168.1.105:7701
Connection: keep-alive
Content-Length: 0
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
X-Custom-Header: foo
Accept: */*
Origin: http://192.168.1.105:8080
Referer: http://192.168.1.105:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: username=Frankie
上面的头信息的 Origin
字段是浏览器自动添加的。
下面是服务器正常的响应报文。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://192.168.1.105:8080
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Expose-Headers: Date,Access-Control-Allow-Origin
Date: Sun, 06 Jun 2021 15:40:11 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 27
上面的头信息中,Access-Control-Allow-Origin
字段是每次回应都必定包含的。
CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。
JSONP 只支持 GET
请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS
的网站请求数据。
以下内容可能与 CORS 无关,不想看可跳过。
Referer
请求头包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer
请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
注意,如果直接在地址栏输入 URL 访问某网页,这时 Referer
为空。
例如,上面示例中请求 /config
接口的网页地址是 http://192.168.1.105:8080/#/about
,从请求报文可以大致看到
Origin
与 Referer
的区别。
Origin: http://192.168.1.105:8080
Referer: http://192.168.1.105:8080/
Origin
的值可以置空或者是 <scheme>://<host> [:<port>]
,即协议 + 主机 + 端口。其中主机是指域名或者 IP
地址;端口号可选,缺省时为服务的默认端口(例如 HTTP 请求默认端口是 80
,HTTPS 请求默认端口是 443
)"#section"
) 和 userinfo (例如
"https://username:password@example.com/foo/bar/"
中的 "username:password"
)。上面的解释比较官方,换句话说就是从哪个页面链接过来的。例如,我从 GitHub 仓库点击跳转至简书文章,可以从 HTML 的请求报文看到:
Host: www.jianshu.com
Referer: https://github.com/toFrankie/csscomb-mini
假设有一个需求是统计从 GitHub 访问简书的访问量,那么就可以通过 Referer
来实现。也常用于防盗链等。
需要注意的是,
Referer
的正确英语拼法是 referrer 。由于早期 HTTP 规范的拼写错误,为保持向下兼容就将错就错了。例如 DOM Level 2、Referrer Policy 等其他网络技术的规范曾试图修正此问题,使用正确拼法,导致目前拼法并不统一。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。