太长若不看,请看这里
我仅仅在这里展示请求处理代码,整个例子可以在这里找到。
我们一个例子开始看。假设我们有一个很棒的网站,为了保护我们的私人·数据,它拥有登陆功能,我们可以在 **/private ** 接口访问到私人数据:
app.get('/private', function(req, res) {
if(req.session.loggedIn === true) {
res.send('THIS IS THE SECRET')
} else {
res.send('Please login first')
}
})
为了不让这个例子变得很复杂,所以我们假设所有用户的密码都是:** secret** ,还有我们将使用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.')
}
})
我们的网站也提供公开接口,比如 ** /public **,用来访问公开数据:
app.get('/public', function(req, res) {
res.send('Public info')
})
从其它域通过AJAX请求我们提供的API
现在虽然我们的API没有经过精心设计,但是我们至少能够从** /public 取到数据。
假设我们的API地址是 ** good.com/public ** , 客户端访问的域名是 ** thirdparty.com , 客户端发起请求的代码如下:
fetch('http://good.com:3000/public')
.then(response => response.text())
.then((result) => {
document.body.textContent = result
})
这段代码没有达到预期效果!
我们可以通过开发者工具来看看 ** http://thirdparty.com ** 下的网络部分:
我们很容易看出虽然请求成功了,但是并没有拿到请求结果。可以从console部分找到原因:
原来如此,我们缺少了* Access-Control-Allow-Origin* 头部,但是为什么我们需要它?它又有什么优点?
同源策略
我们通过 JavaScript 无法获取到请求响应结果是由于同源策略的限制。此策略的目的是确保一个网站不能得到请求其它网站的响应结果。
例如,如果你访问一个网站 ** example.org **,你绝对不会同意这个网站
向你的银行网站发起请求并且拿到你的账户余额数据和交易数据。所以同源策略的意义就在这。
同源策略的“源”是由以下几部分组成:
- 协议(例如: http)
- 主机(例如: example.com)
- 端口(例如: 8000)
所以 ** http://example.org **、 http://www.example.org 和 https://example.org 不同源。
关于CSRF(Cross Site Request Forgery)的一点点知识
我们需要知道有一种叫做跨站请求伪造的攻击方式,它并不受同源策略的影响。
在一次跨站请求伪造攻击中,攻击者一般在背后向第三方网站发请求。例如可以在背后向你的银行网站发起POST方式请求,如果你在本地有银行网址有效的session,任何网站都可以在背后发起请求,除非你的银行有关于CSRF的对策。
我们还需要知道尽管同源策略是有效的,我们例子中从** thirdparty.com成功向 good.com **发起了请求,虽然没有得到响应结果,但是对CSRF攻击来说,并不需要得到响应结果。
让我们的API支持CORS
现在我们的目的是让第三方网站(例如:thirdparty.com)能够得到对我们API的请求结果,我们像错误提示那样设置CORS头部:
app.get('/public', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.send('Public info')
})
我们设置头部“Access-Control-Allow-Origin”为“*”的目的是在浏览器中任何网站都能请求这个URL和拿到请求结果:
不简单的请求和预请求
前面的例子所谓的简单请求,是有着很少头部键值对的GET和POST请求。
我们现在改动一点点我们的API:
app.get('/public', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.send(JSON.stringify({
message: 'This is public info'
}))
})
同时 **thirdparty.com ** 客户端也稍微改变请求,如下:
fetch('http://good.com:3000/public', {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then((result) => {
document.body.textContent = result.message
})
这时我们可以从netwark 板块看到,还是拿不到请求结果:
请求方式不为GET和POST,还有请求Content-Type不为下面三种的任何请求都将拿不到请求结果。
** text/plain **
** application/x-www-form-urlencoded **
** multipart/form-data **
其它Content-Type类型在跨域时都需要预先发起一个预请求。
这种机制的目的是让服务器决定是否允许浏览器发起真正的请求。浏览器设置请求头部 ** 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 ** ,能够通过URL ** /private ** 能够获取到敏感信息。
假如已经把所有CORS设置已经设置好,像 ** evil.com ** 其它网站通过 ** /private ** 能够获取到敏感信息吗?
下面就让我们来看看:
fetch('http://good.com:3000/private')
.then(response => response.text())
.then((result) => {
let output = document.createElement('div')
output.textContent = result
document.body.appendChild(output)
})
无论我们是否登录,都会看到 “Please login first” 的信息。
出现这种情况的原因是 good.com 的 cookie 不会被其它网址的请求所传输,本例中evil.com就是这种情况。
尽管是跨域,但是我们可以让浏览器发送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)
})
此时还是不起作用,不过,这也是一件好事。
想象一下,任何网站都可以向good.com发起认证请求,请求实际发生了但cookie并没有传输过去,请求响应结果也同样拿不到。
所以,我们不想让evil.com能够拿到我们的隐私数据,但是又想让thirdparty.com 能够访问 /private, 我们该如何做?
这种情况下我们应该把响应头部字段 ** Aceess-Control-Allow-Credentials ** 设置为 ** 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 others to make non-authed CORS requests
res.set('Access-Control-Allow-Origin', '*')
}
if(req.session.loggedIn === true) {
res.send('THIS IS THE SECRET')
} else {
res.send('Please login first')
}
})
再次提醒: 不要设置 req.headers.origin 为 Access-Cotroll-Allow-Origin 的值,这样将会允许任何网站向的网站发起认证请求。
也许会有一些例外, 但是在没有白名单情况下实现带cookie的跨域资源共享(CORS)时谨慎考虑。
总结
在本篇文章中,我们回顾了同源策略和我们在需要时如何借助CORS实现跨域请求。
这需要服务端和客户端配合设置,一些基于这些设置的请求会出现有预请求的请求。
另外值得我们需要注意是,处理跨域认证请求时一个白名单能够保证多网站跨域请求而没有泄露敏感数据的风险。
译者注
本文翻译至这里,译者水平有限,错漏缺点在所难免,希望读者批评指正。另:欢迎大家留言讨论。