做者:Martin Splitt前端
譯者:前端小智git
來源:devgithub
爲了縮小代碼量,這裏演示部分代碼,徹底的代碼在 Github 上能夠獲得。web
我們從一個例子開始,假設我們有一個網站,網址爲 http://good.com:8000/public:
json
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'
}))
}
})
複製代碼
目前,我們 API 並非專門設計,但能夠容許其餘人從 /public
URL 中獲取數據。 假設我們的API位於good.com:300/public
上,而且我們的客戶端託管在thirdparty.com
上,該客戶端可能會運行如下代碼:安全
fetch('http://good.com:3000/public')
.then(response => response.text())
.then((result) => {
document.body.textContent = result
})
複製代碼
但這在咱們的瀏覽器中不起做用,經過控制的 network
來看看http://thirdparty.com
的請求:服務器
請求成功,但結果不可用。緣由能夠在控制檯找到:微信
啊哈!我們缺乏Access-Control-Allow-Origin
標頭。 可是,爲何咱們須要它,它有什麼用呢?
咱們在 JS 中得不到響應結果的緣由是同源策略。該策略的目的是確保一個網站不能讀取對另外一個網站的請求的結果,並由瀏覽器強制執行。出於安全方面的考慮,如今的網頁都用cookie
來進行身份驗證,若是不限制讀取,網頁B裏的惡意腳本代碼能夠隨意模仿真實用戶進行操做。
例如: 若是在我們在 example.org
上,並不會但願該網站向咱們的銀行網站發出請求,獲取我們的賬戶餘額和交易。
同源策略能夠防止這種狀況的發生。
在這種狀況下,「來源
」由
example.com
)8000
)CSRF
(跨站點請求僞造) 的說明請注意,有一類攻擊稱爲CSRF(跨站點請求僞造),它沒法經過同源策略來避免。
在CSRF攻擊中,攻擊者向後臺的第三方頁面發出請求,例如向我們的銀行網站發送POST
請求。若是咱們與咱們的銀行存在一個有效的會話,任何網站均可以在後臺發出請求,該請求將被執行,除非我們的銀行網站有針對CSRF
的反措施。
注意,儘管同源策略已經生效,可是的我們的示例請求從thirdparty.com
成功請求到good.com
,只是咱們沒法得到結果。但對於CSRF來講,不須要獲取的結果。
例如,有個 API
經過POST
請求方式發送郵件,返回的內容是我們須要關心的,蛤攻擊者不在意結果,他們關心的是電子郵件是否有發送了成功。
如今,我們但願容許第三方站點(如thirdparty.com
)上的 JS 訪問我們的 API 能獲得響應。爲此,咱們能夠根據錯誤提示啓用CORS
標頭:
app.get('/public', function(req, res) {
res.set('Access-Control-Allow-Origin', '*')
res.send(...)
})
複製代碼
這裏將access-control-allow-origin
標頭設置爲*
,這意味着:容許任何主機訪問此URL和獲取響應的結果:
若是請求不是簡單請求,瀏覽器會先發送一個預請求:
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。
前面的例子是一個的簡單請求。簡單的請求是帶有一些容許的標頭和標誌頭值的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
崩潰了,network
面板向咱們展現了緣由:
瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,"預檢"請求用的請求方法是OPTIONS
,表示這個請求是用來詢問的,頭信息裏面,關鍵字段是Origin
,表示請求來自哪一個源。除了Origin
字段,"預檢"請求的頭信息包括兩個特殊字段。
(1) Access-Control-Request-Method
該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是GET
。
(2) Access-Control-Request-Headers
該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段.
此機制容許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
能夠再次得到響應。
如今,假設我們已登陸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
,都會看到「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)
})
複製代碼
但一樣,這沒法在瀏覽器中工做,其實,這也是個好事。
象一下,任何網站均可以發出通過身份驗證的請求,但不會發送實際的cookie
,而且沒法得到響應。
所以,我們不但願evil.com
可以訪問此私有數據-可是,若是咱們但願thirdparty.com
能夠訪問/ private
,該怎麼辦?
在這種狀況下,須要將Access-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 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來容許跨源請求。
這須要服務器和客戶端設置,而且根據請求會出現預檢請求。
處理通過身份驗證的跨域請求時,應格外當心。 白名單能夠幫助容許多個來源,而不會冒泄露敏感數據(在身份驗證後受到保護)的風險。
編輯中可能存在的bug無法實時知道,過後爲了解決這些bug,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。