了解 CORS
这是从我的博客转发的
TL;DR
- 浏览器强制执行同源策略,以避免从不同源的网站获取响应。
- 同源策略不会阻止向其他来源发出请求,但会禁用 JavaScript 对响应的访问。
- CORS标头允许访问跨域响应。
- CORS 与凭证一起使用时需要谨慎。
- CORS 是浏览器强制执行的策略。其他应用程序不会受其影响。
我们的例子
我将仅在此处展示请求处理代码,但完整的示例可在 Github 上找到。
让我们从一个例子开始。假设我们有一个很棒的网站,它有一个公共 API,位于http://good.com:8000/public
:
app.get('/public', function(req, res) {
res.send(JSON.stringify({
message: 'This is public'
}));
})
我们还有一个简单的登录功能,用户输入一个共享密钥并设置一个 cookie,以标识他们已经过身份验证:
app.post('/login', function(req, res) {
if(req.body.password === 'secret') {
req.session.loggedIn = true
res.send('You are now logged in!')
} else {
res.send('Wrong password.')
}
})
我们使用它来保护我们向用户提供的一些私人数据/private
。
app.get('/private', function(req, res) {
if(req.session.loggedIn === true) {
res.send(JSON.stringify({
message: 'THIS IS PRIVATE'
}))
} else {
res.send(JSON.stringify({
message: 'Please login first'
}))
}
})
通过 AJAX 从其他域请求我们的 API
现在我们的 API 设计得并不特别好,也不是很花哨,但我们可以允许其他人从我们的 URL 获取数据/public
。假设我们的 API 位于 ,good.com:300/public
而我们的客户端托管在thirdparty.com
,客户端可能会运行以下代码:
fetch('http://good.com:3000/public')
.then(response => response.text())
.then((result) => {
document.body.textContent = result
})
但这在我们的浏览器中不起作用!
让我们看一下网络选项卡http://thirdparty.com
:
请求成功,但结果不可用。原因可以在 JavaScript 控制台中找到:
啊哈!我们漏掉了Access-Control-Allow-Origin
标题。但是我们为什么需要它?它有什么用处?
同源策略
我们无法在 JavaScript 中获取响应的原因是同源策略。此策略旨在确保一个网站无法读取来自另一个网站的请求的结果,并由浏览器强制执行。
例如:如果您在线,example.org
您不会希望该网站向您的银行网站发出请求并获取您的帐户余额和交易。
同源策略正好可以防止这种情况发生。
在这种情况下,“起源”由
- 协议(例如
http
) - 主持人(例如
example.com
) - 港口(例如
8000
)
因此http://example.org
和http://www.example.org
和https://example.org
是三个不同的起源。
关于 CSRF 的说明
请注意,有一类攻击称为跨站点请求伪造,同源策略无法缓解这种攻击。
在 CSRF 攻击中,攻击者会在后台向第三方页面发出请求,例如向您的银行网站发送 POST 请求。如果您与银行之间存在有效会话,则任何网站都可以在后台发出请求,除非您的银行采取了针对 CSRF 的应对措施,否则该请求都会被执行。
请注意,尽管同源策略生效,我们示例的请求(来自)thirdparty.com
仍然成功执行good.com
——我们只是无法访问结果。对于 CSRF 来说,我们不需要结果……
例如,如果我们向其提供正确的数据,允许通过 POST 请求发送电子邮件的 API 就会发送电子邮件 - 攻击者并不关心结果,他们关心的是正在发送的电子邮件,而不管是否能够看到 API 响应。
为我们的公共 API 启用 CORS
现在我们确实想允许第三方网站(例如thirdparty.com
)上的 JavaScript 访问我们的 API 响应。为此,我们可以启用 CORS 标头,如错误所述:
app.get('/public', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.send(...)
})
Access-Control-Allow-Origin
这里我们将标头设置*
为:任何主机都可以访问此 URL 以及浏览器中的响应:
非简单请求和预检
前面的例子是所谓的简单请求。简单请求是带有一些允许的标头和标头值的请求GET
。POST
现在thirdparty.com
稍微改变一下实现以获取 JSON:
fetch('http://good.com:3000/public', {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then((result) => {
document.body.textContent = result.message
})
但这又导致 thirdparty.com 崩溃了!
这次网络面板显示了原因:
任何使用非方法GET
或POST
使用非内容类型的请求
text/plain
application/x-www-form-urlencoded
multipart/form-data
简单请求中不允许的任何其他标头都需要预检请求。
此机制旨在让 Web 服务器自行决定是否允许实际请求。浏览器设置Access-Control-Request-Headers
和Access-Control-Request-Method
标头,告知服务器预期的请求,服务器则使用相应的标头进行响应。
我们的服务器尚未回答这些标头,因此我们需要添加它们:
app.get('/public', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Methods', 'GET, OPTIONS')
res.set('Access-Control-Allow-Headers', 'Content-Type')
res.send(JSON.stringify({
message: 'This is public info'
}))
})
现在 thirdparty.com 可以再次访问响应。
凭证和 CORS
现在假设我们已经登录 good.com 并且可以访问/private
包含敏感信息的 URL。
通过我们所有的 CORS 设置,其他站点是否可以evil.com
获取这些敏感信息?
让我们来看看:
fetch('http://good.com:3000/private')
.then(response => response.text())
.then((result) => {
let output = document.createElement('div')
output.textContent = result
document.body.appendChild(output)
})
无论我们是否登录good.com,我们都会看到“请先登录”。
原因是,当请求来自其他来源(在本例中是 evil.com)时,good.com 的 cookie 不会被发送。
我们可以要求浏览器发送 cookie,即使它是跨域域名:
fetch('http://good.com:3000/private', {
credentials: 'include'
})
.then(response => response.text())
.then((result) => {
let output = document.createElement('div')
output.textContent = result
document.body.appendChild(output)
})
但这在浏览器中仍然无法使用。这其实是个好消息。
想象一下任何网站都可以发出经过身份验证的请求 - 请求会被发出但不会发送实际的 cookie,并且响应无法访问。
所以,我们不希望 evil.com 能够访问这些私人数据——但是如果我们想让 thirdparty.com 访问呢/private
?
在这种情况下,我们需要将Access-Control-Allow-Credentials
header 设置为true
:
app.get('/private', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.set('Access-Control-Allow-Credentials', 'true')
if(req.session.loggedIn === true) {
res.send('THIS IS THE SECRET')
} else {
res.send('Please login first')
}
})
但这仍然行不通。允许每个经过身份验证的跨域请求是一种危险的做法。
浏览器不允许我们这么轻易地犯这个错误。
当我们想要允许 thirdparty.com 访问时,/private
我们可以在标头中指定此来源:
app.get('/private', function(req, res) {
res.set('Access-Control-Allow-Origin', 'http://thirdparty.com:8000')
res.set('Access-Control-Allow-Credentials', 'true')
if(req.session.loggedIn === true) {
res.send('THIS IS THE SECRET')
} else {
res.send('Please login first')
}
})
现在http://thirdparty:8000
也可以访问私人数据,而 evil.com 则被锁定。
允许多个来源
现在,我们允许一个源使用身份验证数据进行跨源请求。但是,如果有多个第三方怎么办?
在这种情况下,我们可能需要使用白名单:
const ALLOWED_ORIGINS = [
'http://anotherthirdparty.com:8000',
'http://thirdparty.com:8000'
]
app.get('/private', function(req, res) {
if(ALLOWED_ORIGINS.indexOf(req.headers.origin) > -1) {
res.set('Access-Control-Allow-Credentials', 'true')
res.set('Access-Control-Allow-Origin', req.headers.origin)
} else { // allow other origins to make unauthenticated CORS requests
res.set('Access-Control-Allow-Origin', '*')
}
// let caches know that the response depends on the origin
res.set('Vary', 'Origin');
if(req.session.loggedIn === true) {
res.send('THIS IS THE SECRET')
} else {
res.send('Please login first')
}
})
再次强调:不要直接req.headers.origin
以 CORS 源标头的形式发送。这会允许任何网站访问您网站的已验证请求。
此规则可能存在例外,但在使用没有白名单的凭据实施 CORS 之前,请至少三思。
概括
在本文中,我们研究了同源策略以及如何在需要时使用CORS允许跨源请求。
这需要服务器端和客户端设置,并且根据请求将引发预检请求。
处理经过身份验证的跨域请求时应格外小心。白名单可以帮助允许多个来源,而不会泄露敏感数据(这些数据受到身份验证的保护)。
文章来源:https://dev.to/g33konaut/understanding-cors-aaf