理解 CORS (跨域資源共享)
知識要點
- 瀏覽器強制執行同源策略,拒絕不同站點的網站訪問。
- 同源策略不會阻止對其他源的請求,但是會禁用對js響應的訪問。
- CORS 標頭允許訪問跨域響應。
- CORS 與 Credentials 一起時需要謹慎。
- CORS 是一個瀏覽器強制策略,其他應用程式不受此影響。
事例講解
為了縮小程式碼量,這裡演示部分程式碼,完全的程式碼在Github上可以得到。
咱們從一個例子開始,假設咱們有一個網站,網址為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 並不是專門設計,但可以允許其他人從/publicURL 中獲取資料。 假設咱們的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上,並不會希望該網站向我們的銀行網站發出請求,獲取咱們的帳戶餘額和交易。
同源策略可以防止這種情況的發生。
在這種情況下,“來源”由
- 協議(如http)
- 域名(如example.com)
- 埠(如8000)
關於CSRF(跨站點請求偽造) 的說明
請注意,有一類攻擊稱為CSRF(跨站點請求偽造),它無法通過同源策略來避免。
在CSRF攻擊中,攻擊者向後臺的第三方頁面發出請求,例如向咱們的銀行網站傳送POST請求。如果我們與我們的銀行存在一個有效的會話,任何網站都可以在後臺發出請求,該請求將被執行,除非咱們的銀行網站有針對CSRF的反措施。
注意,儘管同源策略已經生效,但是的咱們的示例請求從thirdparty.com成功請求到good.com,只是我們無法獲得結果。但對於CSRF來說,不需要獲取的結果。
例如,有個API通過POST請求方式傳送郵件,返回的內容是咱們需要關心的,蛤攻擊者不在乎結果,他們關心的是電子郵件是否有傳送了成功。
為咱們的 API 啟用 CORS
現在,咱們希望允許第三方站點(如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可以再次獲得響應。
憑證(credentials)和 CORS
現在,假設咱們已登入good.com並可以使用敏感資訊訪問/privateURL。通過設定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被鎖定了。
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.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來允許跨源請求。
這需要伺服器和客戶端設定,並且根據請求會出現預檢請求。
處理經過身份驗證的跨域請求時,應格外小心。 白名單可以幫助允許多個來源,而不會冒洩露敏感資料(在身份驗證後受到保護)的風險。