1. 程式人生 > 實用技巧 >理解 CORS (跨域資源共享)

理解 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來允許跨源請求。

這需要伺服器和客戶端設定,並且根據請求會出現預檢請求。

處理經過身份驗證的跨域請求時,應格外小心。 白名單可以幫助允許多個來源,而不會冒洩露敏感資料(在身份驗證後受到保護)的風險。