掌握並理解 CORS (跨域資源共享)

做者:Martin Splitt
譯者:前端小智
來源:dev

我的專欄 ES6 深刻淺出已上線,深刻ES6 ,經過案例學習掌握 ES6 中新特性一些使用技巧及原理,持續更新中,←點擊可訂閱。html

點贊再看,養成習慣

本文 GitHub https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。前端

知識要點

  • 瀏覽器強制執行同源策略,拒毫不同站點的網站訪問。
  • 同源策略不會阻止對其餘源的請求,可是會禁用對 JS 響應的訪問。
  • CORS 標頭容許訪問跨域響應。
  • CORS 與 Credentials 一塊兒時須要謹慎。
  • CORS 是一個瀏覽器強制策略,其餘應用程序不受此影響。

事例講解

爲了縮小代碼量,這裏演示部分代碼,徹底的代碼在 Github 上能夠獲得。git

我們從一個例子開始,假設我們有一個網站,網址爲 http://good.com:8000/public:github

app.get('/public', function(req, res) {
  res.send(JSON.stringify({
    message: 'This is public'
  }));
})

我們還有一個簡單的登陸功能,用戶能夠輸入一個共享的密匙並設置一個cookie,以將其標識爲已驗證:web

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 並非專門設計,但能夠容許其餘人從 /public URL 中獲取數據。 假設我們的API位於good.com:300/public上,而且我們的客戶端託管在thirdparty.com上,該客戶端可能會運行如下代碼:json

fetch('http://good.com:3000/public')
  .then(response => response.text())
  .then((result) => {
    document.body.textContent = result
  })

但這在咱們的瀏覽器中不起做用,經過控制的 network 來看看http://thirdparty.com 的請求:跨域

clipboard.png

請求成功,但結果不可用。緣由能夠在控制檯找到:瀏覽器

clipboard.png

啊哈!我們缺乏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和獲取響應的結果:

clipboard.png

非簡單的請求和預檢

若是請求不是簡單請求,瀏覽器會先發送一個預請求:

瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。

前面的例子是一個的簡單請求。簡單的請求是帶有一些容許的標頭和標誌頭值的GETPOST請求。如今,對 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面板向咱們展現了緣由:

clipboard.png

瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的,頭信息裏面,關鍵字段是Origin,表示請求來自哪一個源。除了Origin字段,"預檢"請求的頭信息包括兩個特殊字段。

(1) Access-Control-Request-Method

該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是GET

(2) Access-Control-Request-Headers

該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段.

此機制容許web服務器決定是否容許實際請求。瀏覽器設置Access-Control-Request-HeadersAccess-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並可使用敏感信息訪問 /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.comcookie將不會被髮送,在本例中爲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

原文:https://dev.to/g33konaut/unde...


交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq449245884/xiaozhi

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

相關文章
相關標籤/搜索