了解跨域资源共享(CORS)
跨域资源共享( CORS ) 是一种从一个地方向另一个地方发出 HTTP 请求的方式。历史上,浏览器只允许使用 JavaScript 从同一个域发出请求,并且同源策略会强制执行,从而阻止跨域类型的请求。
CORS 授权服务器控制谁可以发出请求以及允许哪些类型的请求。浏览器是执行 CORS 策略的客户端。
服务器可以配置:
- 哪些域名可以发出 HTTP 请求
- 允许哪些 HTTP 方法(GET、POST、PUT、DELETE 等)。
- 请求中允许使用哪些标头。
- 请求是否可以包含 cookie 信息。
- 客户端可以读取哪些响应标头。
简介 TLDR;
CORS 允许不同主机上的客户端访问服务器上的受限资源。
本教程将涵盖您需要了解的有关 CORS 的所有内容Access-Control-Allow-Origin: *
。是的,有很多东西要学!
同源 vs 跨源
为了简单起见,同源请求就像房子里的两个人进行交流。
由于 Alice 和 Bob 住在同一所房子里(同一个起源),因此他们之间没有沟通障碍。
对比
跨域请求就像有两栋不同的房子,一栋房子里的人与另一栋房子里的人进行交流。
由于Bob想与住在不同房子里的Charlie通信,因此这被视为跨域通信,因为这两个家庭位于不同的源。由于是Bob发起的通话,因此Charlie所在的房子需要批准Bob的通话。
TLDR;
同源请求是指从一个主机向同一主机发出请求,而跨源请求是指从一个主机向不同主机发出请求。
同源请求
举个例子,我们先发起一个同源请求。我们将创建一个简单的服务器,为客户端提供一个端点,并提供一个 HTML 页面。为了简单起见,本教程将使用 Node.js 和Express服务器。
文件server.js
const express = require('express')
const app = express()
const port = 8000
app.use(express.static(__dirname))
app.get('/api/posts', (req, res) => {
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
文件index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
(async () => {
const res = await fetch('http://localhost:8000/api/posts')
console.log(await res.json())
})()
</script>
</body>
</html>
$ node server.js
listening on port 8000
如果你访问http://localhost:8000/ ,你会看到请求成功了。没有什么奇怪的,也很正常,因为是同源的。
跨域请求
现在,为了显示跨域请求,让我们启动一个新服务器来监听不同端口上的 HTML 文件。
文件server2.js
const express = require('express')
const app = express()
const port = 9000
app.use(express.static(__dirname))
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
浏览器抛出错误:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
为了说明为什么会发生这种情况,我们通过一个简单的聊天示例来看看浏览器和服务器如何通信。
浏览器: “你好,服务器http://127.0.0.1:8000,请提供 /api/posts 的数据给我,并告诉我http://localhost:9000上的客户端是否可以访问。以下是 HTTP 消息:”
Get /api/posts HTTTP1.1
User-Agent: Chrome
Host: 127.0.0.1:8000
Accept: */*
Origin: http://localhost:9000
服务器: “这是数据,客户端无法访问,因为我没有将源http://localhost:9000作为允许访问数据的源。”
HTTP/1.1 200 OK
服务器会响应数据,但浏览器不会将其提供给 JavaScript 客户端,除非服务器根据其来源表示允许该客户端。
为了允许跨域请求,服务器必须设置Access-Control-Allow-Origin
响应标头。
现在,我们再尝试在服务器上设置 CORS,为每个请求添加一个新的 header。我们将添加Access-Control-Allow-Origin
一个通配符*
值,告诉浏览器任何来源都可以访问该资源。
文件server.js
const express = require('express')
const app = express()
const port = 8000
app.use(express.static(__dirname))
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
next()
})
app.get('/api/posts', (req, res) => {
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
附注:server2.js
本教程中不会更改,因此您可以继续运行。我们只会server.js
从现在开始进行更改。
好的,现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
为了说明它现在为何有效,让我们再次通过一个简单的聊天示例来看一下浏览器和服务器如何进行通信。
浏览器: “你好,服务器http://127.0.0.1:8000,请提供 /api/posts 的数据给我,并告诉我http://localhost:9000上的客户端是否可以访问。以下是 HTTP 消息:”
Get /api/posts HTTTP1.1
User-Agent: Chrome
Host: 127.0.0.1:8000
Accept: */*
Origin: http://localhost:9000
服务器: “这是数据,任何客户端都可以访问:”
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
访问控制允许来源
最重要的 CORS 标头是:
Origin
请求标头- 响应
Access-Control-Allow-Origin
头
源是主机,由协议、主机名和端口组成:
Origin = protocol + hostname + port
示例协议:
- http
- https
主机名示例:
- 本地主机
- example.com
- foo.example.com
示例端口:
- 80
- 443
- 8000
起源示例:
当无法确定浏览器来源时(例如使用绝对文件路径时),该值null
可以用作有效值Origin
file:///Users/alice/wwww/index.html
浏览器不允许更改原始标头,否则客户端可以假装是其他人。
标头的值Access-Control-Allow-Origin
可以是通配符,也可以是原始值。
有效标头的示例:
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: http://localhost:8000
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Origin: null
您只能有一个值,因此像这样具有多个值是无效的:
# Not valid values, can't have multiple!
Access-Control-Allow-Origin: http://localhost:8000, http://example.com
为了支持多个来源,您必须使用发出请求的当前来源动态替换标头值。
例如,您首先要检查来源是否已列入白名单,然后设置标头:
if (whitelist.contains(req.get('origin')) {
res.set('Access-Control-Allow-Origin', req.get('origin'))
}
对于 localhost 条目,您可以使用正则表达式来匹配所有端口号,例如
正则表达式/^http://\/\/localhost(:\d+)?$/i
匹配
http://localhost
http://localhost:1
http://localhost:10
http://localhost:100
http://localhost:1000
http://localhost:2000 etc
您可以使用硬编码白名单、正则表达式或数据库查询。黑名单总比没有好,但不建议使用。
原产地验证步骤:
Origin
从标题读取值- 使用白名单验证来源
- 将原点设置为
Access-Control-Allow-Origin
null
如果您不介意未设置来源,则可以将其添加到白名单中。
Access-Control-Allow-Origin: null
TLDR;
使用
Access-Control-Allow-Origin
标头告诉浏览器哪些来源被允许访问数据。
预检请求
对于某些 HTTP 方法,浏览器会通过所谓的预检请求来请求权限。如果浏览器批准预检请求的响应,则会发出实际请求。如果浏览器不批准预检请求的响应,则永远不会发出实际请求。
触发预检请求的 HTTP 方法有:
PUT
PATCH
DELETE
TRACE
不触发预检请求的 HTTP 方法称为简单请求,它们包括:
HEAD
GET
POST
预检请求是在主请求之前由浏览器向服务器发出的一个小请求,其中包含诸如所使用的 HTTP 方法以及是否存在任何 HTTP 标头等信息。服务器会通过返回 2xx HTTP 状态代码来判断浏览器是否应该发送实际请求,或者返回一个指示客户端不应发送实际请求的错误。
预检请求可防止服务器接收不必要的跨域请求。如果服务器启用了 CORS,它将知道如何处理预检请求并做出相应的响应。如果服务器不理解或不关心 CORS,则不会发送正确的预检响应。
预检请求使用OPTIONS
HTTP 方法。
在以下情况下会触发预检:
-
GET
客户端请求正在使用除、POST
或之外的方法HEAD
(称为简单方法) -
Content-Type
客户端使用除以下值以外的值设置请求标头:application/x-www-form-urencoded
multipart/form-data
text/plain
-
客户端设置了以下附加请求标头:
Accept
Accept-Language
Content-Language
预检响应应在200
范围之内(HTTP 代码 204 为标准),并且不包含正文。包含正文也会让开发人员感到困惑。
让我们发出一个DELETE
请求来演示预检请求。首先,我们将检查该请求是否为预检请求。如果是,则返回 HTTP204 No Content
状态码。
文件server.js
const express = require('express')
const app = express()
const port = 8000
const isPreflight = (req) => {
return (
req.method === 'OPTIONS' &&
req.headers['origin'] &&
req.headers['access-control-request-method']
)
}
app.use(express.static(__dirname))
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.status(204).end()
return
}
next()
})
app.get('/api/posts', (req, res) => {
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.delete('/api/posts', (req, res) => {
res.json({success: true})
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
文件index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
(async () => {
const res = await fetch('http://localhost:8000/api/posts', {
method: 'DELETE'
})
console.log(await res.json())
})()
</script>
</body>
</html>
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
我们可以看到错误提示,该方法DELETE
不被 CORS 允许:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.
请记住,诸如、或之类的简单请求GET
不会POST
启动HEAD
预检请求,并且它们会被 CORS 自动允许,但诸如DELETE
或之类的方法PUT
需要服务器明确允许。
为了允许DELETE
客户端执行该方法,我们需要Access-Control-Allow-Methods
在预检请求中添加响应标头。
文件server.js
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE') // Add this!
res.status(204).end()
return
}
next()
})
现在请求DELETE
可以按预期工作了:
请注意两个屏幕截图中请求的数量不同。
- 在第一个屏幕截图中,预检请求失败,因此实际
DELETE
请求从未发出,仅创建了 1 个请求。
- 在第二个屏幕截图中,预检请求成功,因此
DELETE
发出了实际请求,创建了 2 个请求。
要拒绝预检请求:
- 省略
Access-Control-Allow-Origin
标题。 Access-Control-Allow-Methods
返回与标题不匹配的值Access-Control-Request-Method
。- 如果预检请求有
Access-Control-Request-Header
- 省略
Access-Control-Allow-Headers
标题。 - 返回一个值
Access-Control-Allow-Headers
返回与标头不匹配的标头Access-Control-Request-Headers
。
- 省略
预检响应和实际响应都需要Access-Control-Allow-Origin
标头。
浏览器可能会缓存对该来源的资源的第一个请求的预检响应,以避免一直发送额外的查询。
预检是无状态的,这意味着实际请求不包含任何将其与预检请求联系起来的信息。
预检请求永远不会遵循重定向。如果您尝试发出预检请求,但服务器尝试重定向,则预检请求将失败。您可以手动检查Location
标头,以了解服务器尝试将您重定向到的位置。只有简单的 CORS 请求(GET、POST、HEAD)才会遵循重定向。
如果重定向是同一个服务器,那么Origin
标头将保持不变,否则将设置为null
。
TLDR;
预检请求根据服务器允许的内容确定客户端是否有权发出实际请求。该请求必须是一个
OPTIONS
方法,具有Access-Control-Request-Method
标头(例如DELETE
或PUT
),并且包含Origin
标头才会被视为预检请求。
访问控制请求方法
这Access-Control-Request-Method
是单个请求标头值,用于请求使用特定 HTTP 方法的权限。当客户端发送非简单方法请求时,浏览器会设置此标头。
这Access-Control-Request-Method
仅在预检请求中发送
下面的屏幕截图显示了Access-Control-Request-Method
预检请求中的请求标头,并显示了Access-Control-Allow-Methods
我们之前执行请求时的预检响应中的响应标头DELETE
。
使用准确的预检标头来保护您的服务器免受意外请求的影响。如果您的服务器只允许GET
请求,则不要将其他标头放在Access-Control-Allow-Methods
标头中添加其他标头。
TLDR;
浏览器会
Access-Control-Request-Method
在预检请求中发送请求头,告知服务器它打算在实际请求中使用请求的方法。如果服务器允许该方法,浏览器就会发出实际请求。
访问控制允许方法
这Access-Control-Allow-Methods
预检请求,以告知客户端哪些方法允许使用 CORS。
例如,Access-Control-Allow-Methods: DELETE
标头指示服务器允许客户端进行DELETE
向 URL 发出请求。
请求头Access-Control-Allow-Methods
可以有多个值,例如:
Access-Control-Allow-Methods: HEAD, GET, POST, PUT, PATCH, DELETE
请记住GET
,、、POST
和HEAD
是简单的方法并且始终允许,因此将它们放在标题中是多余且不必要的,但有些人喜欢将它们放在标题中,因为这样对他们来说更清楚并避免混淆。
示例显示允许使用 PUT 和 DELETE(除了简单方法):
TLDR;
服务器应该通过
Access-Control-Allow-Methods
响应头进行响应,让浏览器知道客户端可以执行哪些 HTTP 方法。
访问控制请求标头
请求Access-Control-Request-Headers
标头在预检请求中发送,以便让服务器知道客户端将在实际响应中发送哪些标头。
让我们尝试向服务器发送自定义标头。
文件index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
(async () => {
const res = await fetch('http://localhost:8000/api/posts', {
headers: new Headers({
'My-Custom-Header': 'hello world'
})
})
console.log(await res.json())
})()
</script>
</body>
</html>
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
浏览器响应错误:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: Request header field my-custom-header is not allowed by Access-Control-Allow-Headers in preflight response.
由于浏览器强制执行 CORS 策略,因此 JavaScript 无法设置的标头包括:
Accept-Charset
Accept-Encoding
Access-Control-Request-Headers
Access-Control-Request-Method
Connection
Content-Length
Cookie
Cookie2
Date
DNT
Expect
Host
Keep-Alive
Origin
Referer
TE
Trailer
Transfer-Encoding
Upgrade
User-Agent
Via
Proxy-
以或开头的标题Sec-
这些标头只能由浏览器设置,因为它们具有特殊含义。浏览器会忽略您为这些标头设置的值。
对于其他标头,服务器必须允许客户端在跨源请求中包含自定义请求标头。
我们可以看到,浏览器在预检请求中通过以下标头请求访问自定义标头的权限Access-Control-Request-Headers
:
服务器需要将批准的自定义请求标头列入白名单,如果没有将请求标头列入白名单,则请求将失败。
如果执行同源请求,则请求可以包含任何自定义请求标头,因为来源是受信任的。
默认情况下,CORS 仅允许客户端读取这些响应标头:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
如果服务器设置了额外的响应头,客户端将无法看到它们。为了让客户端能够看到额外的响应头,服务器需要将这些响应头暴露出来,我们稍后会讲到。
因此,为了让服务器能够接受客户端的自定义标头,它需要通过Access-Control-Allow-Headers
在预检响应中设置响应标头来明确允许该标头。
文件server.js
const express = require('express')
const app = express()
const port = 8000
const isPreflight = (req) => {
return (
req.method === 'OPTIONS' &&
req.headers['origin'] &&
req.headers['access-control-request-method']
)
}
app.use(express.static(__dirname))
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header') // Add this!
res.status(204).end()
return
}
next()
})
app.get('/api/posts', (req, res) => {
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.delete('/api/posts', (req, res) => {
res.json({success: true})
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
预检使用允许的标头进行响应,因此浏览器继续发送自定义请求标头进行实际请求。
请记住,以 开头的标头Access-Control-Request-
是浏览器向服务器请求权限的请求标头,以 开头的标头Access-Control-Allow-
是服务器向浏览器授予权限的响应标头。
Access-Control-Allow-Headers
仅需出现在飞行前响应中。
TLDR;
浏览器
Access-Control-Request-Headers
在预检请求中发送一个请求标头,让服务器知道实际请求将请求指定的标头。如果服务器拒绝请求这些标头,则不会发送实际请求。
访问控制公开标头
标Access-Control-Allow-Headers
头由预检使用来指示请求中允许哪些标头,而Access-Control-Expose-Headers
标头由实际响应使用来指示哪些响应标头对客户端可见。
如果服务器未设置公开标头,则客户端将无法读取响应标头。
始终向客户端公开的标头是简单的标头,它们是:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
作为示例,让我们尝试读取客户端上的所有响应头:
文件index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
(async () => {
const res = await fetch('http://localhost:8000/api/posts')
console.log(Array.from(await res.headers.entries()))
})()
</script>
</body>
</html>
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
没有什么令人惊讶的,我们得到了标准标题。
让我们在服务器端设置一个自定义响应头。
const express = require('express')
const app = express()
const port = 8000
const isPreflight = (req) => {
return (
req.method === 'OPTIONS' &&
req.headers['origin'] &&
req.headers['access-control-request-method']
)
}
app.use(express.static(__dirname))
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
res.status(204).end()
return
} else {
res.set('Timezone-Offset', '240') // Add this!
}
next()
})
app.get('/api/posts', (req, res) => {
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.delete('/api/posts', (req, res) => {
res.json({success: true})
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
等等,我们看到的只是相同的标头。新的标头并没有提供。这是因为服务器需要使用响应标头来设置允许客户端读取哪些标头Access-Control-Expose-Headers
。
让我们添加Access-Control-Expose-Headers
一个常规响应标头来告诉浏览器客户端被允许读取自定义标头:
文件server.js
const express = require('express')
const app = express()
const port = 8000
const isPreflight = (req) => {
return (
req.method === 'OPTIONS' &&
req.headers['origin'] &&
req.headers['access-control-request-method']
)
}
app.use(express.static(__dirname))
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
res.status(204).end()
return
} else {
res.set('Access-Control-Expose-Headers', 'Timezone-Offset') // Add this!
res.set('Timezone-Offset', '240')
}
next()
})
app.get('/api/posts', (req, res) => {
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.delete('/api/posts', (req, res) => {
res.json({success: true})
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
我们现在可以在客户端读取服务器发送到浏览器的自定义标头。
TLDR;
用于
Access-Control-Expose-Headers
允许客户端读取额外的非简单标头。
访问控制最大年龄
标Access-Control-Max-Age
头指示预检响应应缓存多长时间(以秒为单位)。让我们通过Access-Control-Max-Age
在预检响应中设置标头来告诉浏览器将预检响应缓存 2 分钟(120 秒)。
文件server.js
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
res.set('Access-Control-Max-Age', '120') // Add this!
res.status(204).end()
return
} else {
res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
res.set('Timezone-Offset', '240')
}
next()
})
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
我们可以Access-Control-Max-Age
在预检响应头中看到该头:
Firefox 不允许缓存项目超过 24 小时,而 Chrome、Opera 和 Safari 最多缓存项目 5 分钟。如果Access-Control-Max-Age
未指定,则 Firefox 不会缓存预检,而 Chrome、Opera 和 Safari 会缓存预检 5 秒。
通过减少网络请求的数量来最大化Access-Control-Max-Age
标头以获得更好的移动体验。
为了防止代理服务器缓存来自一个客户端的响应并将其发送给另一个客户端,请使用Vary: Origin
响应标头来指示将有所Access-Control-Allow-Origin
不同,不应缓存
Vary: Origin
例如:
if (isPreflight(req)) {
if (whitelist.contains(req.get('origin'))) {
res.set('Access-Control-Allow-Origin', req.get('origin'))
}
res.set('Vary', 'Origin')
} else {
res.set('Access-Control-Allow-Origin', '*')
}
TDLR;
告诉
Access-Control-Max-Age
浏览器缓存预检响应的时间(以秒为单位)。
访问控制允许凭证
该Access-Control-Allow-Credentials
标头用于允许客户端在跨源请求中发送 cookie 等敏感信息。
由于 Cookie 包含敏感信息,出于安全考虑,需要进行额外配置。如果始终启用“选择加入”功能,意外将个人信息发送到其他来源的网站将非常危险。
JavaScript document.cookie
API 无法从其他来源读取或写入该值。调用document.cookie
仅返回客户端自身的 Cookie,而不会返回跨源 Cookie。Cookie 本身使用同源策略,每个 Cookie 都包含一个路径和一个域名,只有与路径和域名匹配的页面才能读取该 Cookie。
Cookie 在以下情况下最有效:
- 您希望在自己的客户端和服务器生态系统内授权用户。
- 您确切地知道哪些客户端将访问您的服务器。
网站通过用户凭证识别用户,其中最常用的形式是 Cookie。服务器使用 Cookie 来存储用于标识用户使用情况的唯一标识符,例如与用户 ID 绑定的会话 ID。
同源HTTP请求总会包含cookie,但跨源请求默认不包含cookie。
在客户端启用凭证选项来发送:
- 曲奇饼
- 基本身份验证
- 客户端 SSL 证书
让我们在服务器上设置一个 Cookie,并在客户端读取它。在本例中,我们将在帖子的 GET 请求中执行此操作。Cookie 的响应头将是一个简单的键/值,可以从任何路径读取。
Set-Cookie: username=alice; Path=/
文件server.js
const express = require('express')
const app = express()
const port = 8000
const isPreflight = (req) => {
return (
req.method === 'OPTIONS' &&
req.headers['origin'] &&
req.headers['access-control-request-method']
)
}
app.use(express.static(__dirname))
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*')
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
res.set('Access-Control-Max-Age', '120')
res.status(204).end()
return
} else {
res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
res.set('Timezone-Offset', '240')
}
next()
})
app.get('/api/posts', (req, res) => {
res.set('Set-Cookie', 'username=alice; Path=/') // Add this!
res.json([
{id: 1, content: 'foo'},
{id: 1, content: 'bar'},
])
})
app.delete('/api/posts', (req, res) => {
res.json({success: true})
})
app.listen(port, () => {
console.log(`listening on port ${port}`)
})
文件index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
(async () => {
const res = await fetch('http://localhost:8000/api/posts')
console.log(document.cookie)
})()
</script>
</body>
</html>
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
什么都没记录!这是因为跨域请求默认允许读取 Cookie。我们可以通过credentials: 'include'
在 fetch 请求选项中设置来告诉服务器我们希望读取 Cookie。
在 JavaScript fetch 调用中,您需要设置credentials: 'include'
为在跨域请求时发送 Cookie。如果使用XMLHttpRequest
,则需要设置withCredentials: true
。
文件index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script>
(async () => {
const res = await fetch('http://localhost:8000/api/posts', {
credentials: 'include'
})
console.log(document.cookie)
})()
</script>
</body>
</html>
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
我们得到错误:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
这意味着为了让客户端读取 cookie,Access-Control-Allow-Origin
不能使用通配符*
,并且必须明确设置为允许读取 cookie 的来源。
很简单,我们可以动态地将允许的来源设置为请求标头中的来源。在实际应用中,为了更安全,最好将允许读取 Cookie 的来源设置为白名单。
文件server.js
app.use((req, res, next) => {
// res.set('Access-Control-Allow-Origin', '*') // remove this!
res.set('Access-Control-Allow-Origin', req.get('origin')) // Add this!
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
res.set('Access-Control-Max-Age', '120')
res.status(204).end()
return
} else {
res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
res.set('Timezone-Offset', '240')
}
next()
})
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
我们得到以下错误:
Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
这是因为浏览器需要知道服务器允许从跨源请求中读取 cookie。
为了启用 cookie 支持,服务器必须Access-Control-Allow-Credentials
设置标头,true
以表明允许客户端读取 cookie 以及服务器允许接收 cookie。
标题唯一Access-Control-Allow-Credentials
可以具有的值是true
。
Access-Control-Allow-Credentials: true
文件server.js
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', req.get('origin'))
if (isPreflight(req)) {
res.set('Access-Control-Allow-Methods', 'DELETE')
res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
res.set('Access-Control-Max-Age', '120')
res.status(204).end()
return
} else {
res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
res.set('Access-Control-Allow-Credentials', 'true') // Add this!
res.set('Timezone-Offset', '240')
}
next()
})
重启服务器:
$ node server.js
listening on port 8000
$ node server2.js
listening on port 9000
现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么
客户端现在可以读取 cookie!
我们可以看到Access-Control-Allow-Credentials
设置为true
并Access-Control-Allow-Origin
设置为实际来源而不是通配符,这两个标头都是客户端允许使用 cookie 所必需的。
标Access-Control-Allow-Credentials
头可以出现在预检请求和实际请求中,但 Cookie 只会在实际请求中发送。只有非预检响应才需要标头。
TLDR;
设置响应标头
Access-Control-Allow-Credentials: true
以允许客户端读取 Cookie 等敏感信息。使用credentials: 'include' if using the fetch API or use
withCredentials: trueif using the XMLHttpRequest API.
Access-Control-Allow-Origin 请求凭据时,不能使用通配符。
CSRF
CSRF 令牌是客户端和服务器之间共享的不可猜测的令牌,用于防御跨站请求伪造(CSRF) 攻击。服务器将页面以令牌作为标头提供给客户端,客户端在每次请求时都发送 CSRF 令牌。如果服务器无法验证令牌,则请求无效。当客户端使用 Cookie 请求受保护的数据时,需要使用拒绝的 CSRF 保护。
使用 cURL 进行欺骗Origin
是可能的并且很容易,但使用 cookie 进行欺骗则比较困难,因为 cURL 无法直接或远程访问浏览器 cookie。
在可能的情况下,考虑同源请求而不是使用 CSRF,因为它更安全。
如果构建公共 API,请使用 cookie 以外的其他方式来验证用户,例如使用 Oauth2。
浏览器支持
CORS 完全支持:
- Chrome 3+
- Firefox 3.5+
- Safari 4+
- IE 10+
- Opera 12+
- iOS 3.2+
- Android 2.1+
结论
我们介绍了 CORS 的各种标头,包括:
Access-Control-Allow-Origin
Access-Control-Request-Method
Access-Control-Request-Headers
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Expose-Headers
Access-Control-Allow-Credentials
Access-Control-Max-Age
添加 CORS 支持时需要问的问题:
- 为什么服务器需要支持跨域请求?
- CORS 是否被添加到新服务或现有服务中?
- 哪些客户端应该有权访问该网站?
- 用户将使用哪些浏览器和设备访问该网站?
- 服务器支持哪些 HTTP 方法和标头?
- API 是否应该支持用户特定数据?
- 是否使用 cookies 来验证用户身份?
TLDR;
仅在绝对必要时才允许跨域请求,并且明确说明允许哪些来源、方法、标头和凭据。
资源:
文章来源:https://dev.to/miguelmota/understanding-cross-origin-resource-sharing-cors-2i3e