CORS(Cross-origin resource sharing)的介紹

太長若不看,請看這里

  • 同源策略其實并不阻止其它域的請求, 而是使JavaScript不能獲取到響應(yīng)。
  • CORS設(shè)置頭部可以得到跨域響應(yīng)。
  • 與證書相關(guān)的CORS值得注意。

我僅僅在這里展示請求處理代碼,整個例子可以在這里找到

我們一個例子開始看。假設(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.orghttps://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)險。

譯者注

本文翻譯至這里,譯者水平有限,錯漏缺點在所難免,希望讀者批評指正。另:歡迎大家留言討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容