太長若不看,請看這里
我僅僅在這里展示請求處理代碼,整個例子可以在這里找到。
我們一個例子開始看。假設(shè)我們有一個很棒的網(wǎng)站,為了保護(hù)我們的私人·數(shù)據(jù),它擁有登陸功能,我們可以在 **/private ** 接口訪問到私人數(shù)據(jù):
app.get('/private', function(req, res) {
if(req.session.loggedIn === true) {
res.send('THIS IS THE SECRET')
} else {
res.send('Please login first')
}
})
為了不讓這個例子變得很復(fù)雜,所以我們假設(shè)所有用戶的密碼都是:** secret** ,還有我們將使用cookie來保存我們的私人數(shù)據(jù):
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.')
}
})
我們的網(wǎng)站也提供公開接口,比如 ** /public **,用來訪問公開數(shù)據(jù):
app.get('/public', function(req, res) {
res.send('Public info')
})
從其它域通過AJAX請求我們提供的API
現(xiàn)在雖然我們的API沒有經(jīng)過精心設(shè)計,但是我們至少能夠從** /public 取到數(shù)據(jù)。
假設(shè)我們的API地址是 ** good.com/public ** , 客戶端訪問的域名是 ** thirdparty.com , 客戶端發(fā)起請求的代碼如下:
fetch('http://good.com:3000/public')
.then(response => response.text())
.then((result) => {
document.body.textContent = result
})
這段代碼沒有達(dá)到預(yù)期效果!
我們可以通過開發(fā)者工具來看看 ** http://thirdparty.com ** 下的網(wǎng)絡(luò)部分:

我們很容易看出雖然請求成功了,但是并沒有拿到請求結(jié)果??梢詮腸onsole部分找到原因:

原來如此,我們?nèi)鄙倭? Access-Control-Allow-Origin* 頭部,但是為什么我們需要它?它又有什么優(yōu)點?
同源策略
我們通過 JavaScript 無法獲取到請求響應(yīng)結(jié)果是由于同源策略的限制。此策略的目的是確保一個網(wǎng)站不能得到請求其它網(wǎng)站的響應(yīng)結(jié)果。
例如,如果你訪問一個網(wǎng)站 ** example.org **,你絕對不會同意這個網(wǎng)站
向你的銀行網(wǎng)站發(fā)起請求并且拿到你的賬戶余額數(shù)據(jù)和交易數(shù)據(jù)。所以同源策略的意義就在這。
同源策略的“源”是由以下幾部分組成:
- 協(xié)議(例如: http)
- 主機(jī)(例如: example.com)
- 端口(例如: 8000)
所以 ** http://example.org **、 http://www.example.org 和 https://example.org 不同源。
關(guān)于CSRF(Cross Site Request Forgery)的一點點知識
我們需要知道有一種叫做跨站請求偽造的攻擊方式,它并不受同源策略的影響。
在一次跨站請求偽造攻擊中,攻擊者一般在背后向第三方網(wǎng)站發(fā)請求。例如可以在背后向你的銀行網(wǎng)站發(fā)起POST方式請求,如果你在本地有銀行網(wǎng)址有效的session,任何網(wǎng)站都可以在背后發(fā)起請求,除非你的銀行有關(guān)于CSRF的對策。
我們還需要知道盡管同源策略是有效的,我們例子中從** thirdparty.com成功向 good.com **發(fā)起了請求,雖然沒有得到響應(yīng)結(jié)果,但是對CSRF攻擊來說,并不需要得到響應(yīng)結(jié)果。
讓我們的API支持CORS
現(xiàn)在我們的目的是讓第三方網(wǎng)站(例如:thirdparty.com)能夠得到對我們API的請求結(jié)果,我們像錯誤提示那樣設(shè)置CORS頭部:
app.get('/public', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.send('Public info')
})
我們設(shè)置頭部“Access-Control-Allow-Origin”為“*”的目的是在瀏覽器中任何網(wǎng)站都能請求這個URL和拿到請求結(jié)果:

不簡單的請求和預(yù)請求
前面的例子所謂的簡單請求,是有著很少頭部鍵值對的GET和POST請求。
我們現(xiàn)在改動一點點我們的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 板塊看到,還是拿不到請求結(jié)果:

請求方式不為GET和POST,還有請求Content-Type不為下面三種的任何請求都將拿不到請求結(jié)果。
** text/plain **
** application/x-www-form-urlencoded **
** multipart/form-data **
其它Content-Type類型在跨域時都需要預(yù)先發(fā)起一個預(yù)請求。
這種機(jī)制的目的是讓服務(wù)器決定是否允許瀏覽器發(fā)起真正的請求。瀏覽器設(shè)置請求頭部 ** Access-Control-Request-Headers ** 和 ** Access-Control-Request-Method ** 后,服務(wù)器便能知道瀏覽器所希望返回的數(shù)據(jù),同時服務(wù)器也需要返回響應(yīng)請求頭部字段。
我們現(xiàn)在還沒有返回響應(yīng)請求頭部字段,所以需要增加這些:
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'
}))
})
現(xiàn)在,** thirdparty.com ** 便能夠獲取到請求響應(yīng)返回數(shù)據(jù)。
憑證和CORS
假設(shè)我們已經(jīng)登錄進(jìn)了 ** good.com ** ,能夠通過URL ** /private ** 能夠獲取到敏感信息。
假如已經(jīng)把所有CORS設(shè)置已經(jīng)設(shè)置好,像 ** evil.com ** 其它網(wǎng)站通過 ** /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” 的信息。
出現(xiàn)這種情況的原因是 good.com 的 cookie 不會被其它網(wǎng)址的請求所傳輸,本例中evil.com就是這種情況。
盡管是跨域,但是我們可以讓瀏覽器發(fā)送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)
})
此時還是不起作用,不過,這也是一件好事。
想象一下,任何網(wǎng)站都可以向good.com發(fā)起認(rèn)證請求,請求實際發(fā)生了但cookie并沒有傳輸過去,請求響應(yīng)結(jié)果也同樣拿不到。
所以,我們不想讓evil.com能夠拿到我們的隱私數(shù)據(jù),但是又想讓thirdparty.com 能夠訪問 /private, 我們該如何做?
這種情況下我們應(yīng)該把響應(yīng)頭部字段 ** Aceess-Control-Allow-Credentials ** 設(shè)置為 ** 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')
}
})
但是這樣還是不可行,** 允許所有域名都可以發(fā)起跨域認(rèn)證請求是一個危險的動作 **。
瀏覽器不會輕易允許這種錯誤發(fā)生。
當(dāng)我們想讓 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')
}
})
現(xiàn)在,http://thirdparty:8000 也可以獲取到隱私數(shù)據(jù)了,但是 evil.com 依然不可以。
允許多域名訪問
現(xiàn)在我們已經(jīng)允許了一個域名能夠發(fā)起跨域認(rèn)證請求,我們?nèi)绾胃嘤蛎兀?/p>
這種情況下,我們可以想到使用白名單:
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')
}
})
再次提醒: 不要設(shè)置 req.headers.origin 為 Access-Cotroll-Allow-Origin 的值,這樣將會允許任何網(wǎng)站向的網(wǎng)站發(fā)起認(rèn)證請求。
也許會有一些例外, 但是在沒有白名單情況下實現(xiàn)帶cookie的跨域資源共享(CORS)時謹(jǐn)慎考慮。
總結(jié)
在本篇文章中,我們回顧了同源策略和我們在需要時如何借助CORS實現(xiàn)跨域請求。
這需要服務(wù)端和客戶端配合設(shè)置,一些基于這些設(shè)置的請求會出現(xiàn)有預(yù)請求的請求。
另外值得我們需要注意是,處理跨域認(rèn)證請求時一個白名單能夠保證多網(wǎng)站跨域請求而沒有泄露敏感數(shù)據(jù)的風(fēng)險。
譯者注
本文翻譯至這里,譯者水平有限,錯漏缺點在所難免,希望讀者批評指正。另:歡迎大家留言討論。