今天故事的主角還是大家熟識的二狗子。二狗子拿到了一筆專案獎金,在好好犒勞了自己一頓後,決定把剩下的錢在銀行存個定期。
他用瀏覽器訪問了 www.bank.com,輸入了使用者名稱和密碼後,成功登入。
bank.com 返回了 cookie 用來標識二狗子這個使用者。
不得不說,瀏覽器是個認真負責的工具,它會把這個 cookie 記錄下來,以後二狗子每次向 bank.com 發起 HTTP 請求,瀏覽器都會準確無誤地把 cookie 加入到 HTTP 請求頭部中,一起傳送到 bank.com,這樣 bank.com 就知道二狗子已經登陸過了,就可以按照二狗子的請求來做事情,比如檢視餘額、轉賬取錢。
二狗子存完錢,看著賬戶餘額,心中暗喜。於是,他開啟了 www.meinv.com,去看自己喜歡的電影。
但二狗子不知道的是,瀏覽器把 meinv.com 的 HTML、JavaScript 都下載到本地,開始執行。而其中某個 JavaScript 中,偷偷建立了一個 XMLHttpRequest 物件,然後向 bank.com 發起了 HTTP 請求 。
瀏覽器嚴格按照規定,把之前儲存的 cookie 也新增到 HTTP 請求中。但是 bank.com 根本不知道這個 HTTP 請求是 meinv.com 的 JavaScript 發出的,還以為是二狗子發出的。bank.com 檢查了cookie,發現這是一個登入過的使用者,於是兢兢業業地去執行請求命令,二狗子的個人資訊就洩露了。(ps. 實際中實施這樣一次攻擊不會這麼簡單,銀行網站肯定是做了其他很多安全校驗的措施,本故事只是用來說明基本原理。)
可憐的二狗子還不知道發生了什麼,已經遭受了錢財損失。那我們來幫他覆盤一下為什麼會發生這種情況。
首先,每當訪問 bank.com 的時候,不管是人點選按鈕訪問連結,還是透過程式的方式,儲存在瀏覽器的 bank.com 的 cookie 都會進行傳遞。
其次,從 meinv.com 下載的 JavaScript 利用 XMLHttp 訪問了 bank.com。
第一點我們是無法阻止的,如果阻止了,cookie 就喪失了它的主要作用。
對於第二點,瀏覽器必須做出限制,不能讓來自 meinv.com 的 JavaScript 去訪問 bank.com。這個限制就是同源策略。
同源策略
瀏覽器提供了 fetch API 或 XMLHttpRequest 等方式,它們可以使我們方便快捷地向後端發起請求,取得資源,展示在前端上。而透過 fetch API 或 XMLHttpRequest 等方式發起的 HTTP 請求,就必須要遵守同源策略 。
那什麼是同源策略呢?同源策略(same-origin policy)規定了當瀏覽器使用 JavaScript 發起 HTTP 請求時,如果是請求域名同源的情況下,請求不會受到限制。但如果是非同源的請求,則會強制遵守 CORS (Cross-Origin Resource Sharing,跨源資源共享) 的規範,否則瀏覽器就會將請求攔截。
那什麼情況下是同源呢?同源策略非常嚴格,要求兩個 URL 必須滿足下面三個條件才算同源:
1、協議(http/https)相同;
2、域名(domain)相同;
3、埠(port)相同。
舉個例子:下列哪些 URL 地址與 https://www.bank.com/withdraw... 屬於同源?
- https://www.bank.com/save.html (✅)
- http://www.bank.com/withdraw.... (❌,協議不同)
- https://bank.com/login.html (❌,域名不同)
- https://www.bank.com:8080/wit... (❌,埠不同)
因此,當我們請求不同源的 URL 地址時,就會產生一個跨域 HTTP 請求(cross-origin http request)。
例如想要在 https://www.upyun.com 的頁面上顯示來自 https://opentalk.upyun.com 的資訊內容,我們使用瀏覽器提供的 fetch API 來發起一個請求:
try {
fetch('https://opentalk.upyun.com/data')
} catch (err) {
console.error(err);
}
這就產生了一個跨域請求,跨域請求則必須遵守 CORS 的規範。
當請求的伺服器沒有配置允許 CORS 訪問或者不允許來源地址的話,請求就會失敗,在 Chrome 的開發者工具臺上就會看到以下的經典錯誤:
Access to fetch at 'https://opentalk.upyun.com/data' from origin 'https://www.upyun.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
那在實際應用中,我們該如何正確地設定 CORS 呢?
什麼是 CORS
CORS 是針對不同源(域)的請求而制定的規範。瀏覽器在請求不同域的資源時,被跨域請求的服務端必須明確地告知瀏覽器其允許何種請求。只有在伺服器允許範圍內的請求才能夠被瀏覽器放行並請求,否則會被瀏覽器攔截,訪問失敗。
在 CORS 規範中,跨域請求主要分為兩種:簡單請求(simple request)和非簡單請求(not-so-simple request)。
簡單請求
簡單請求必須符合以下四個條件,實際開發中我們一般只關注前面兩個條件:
(1)使用 GET、POST、HEAD 其中一種方法;
(2)只使用瞭如下的安全請求頭部,不得人為設定其他請求頭部:
- Accept
- Accept-Language
- Content-Language
Content-Type 僅限以下三種:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
(3)請求中的任意 XMLHttpRequestUpload 物件均沒有註冊任何事件監聽器,XMLHttpRequestUpload 物件可以使用 XMLHttpRequest.upload 屬性訪問;
(4)請求中沒有使用 ReadableStream 物件。
不符合以上任一條件的請求就是非簡單請求。瀏覽器對於簡單請求和非簡單請求,處理的方式也不一樣。
對於簡單請求,瀏覽器會直接發出 CORS 請求。具體來說,就是在請求頭資訊中,自動地增加一個Origin (來源)欄位。
Origin 的值中,包含請求協議、域名和埠三個部分,用於說明本次請求來自哪個源。伺服器可以根據這個值,決定是否同意這次請求。例如下面的請求頭報文:
GET /data HTTP/2
Host: opentalk.upyun.com
accept-encoding: deflate, gzip
accept: */*
origin: https://www.upyun.com
......
如果 Origin 指定的源不在伺服器允許範圍內,伺服器會返回響應一個正常的 HTTP,瀏覽器發現回應頭部中,如果沒有包含 Access-Control-Allow-Origin 欄位,就會丟擲錯誤。需要注意的是,這種錯誤無法透過狀態碼識別,HTTP 響應的狀態碼有可能是 200。
如果 Origin 指定的源在允許範圍內的話,響應頭部中,就會有以下幾個欄位:
Access-Control-Allow-Origin: https://www.upyun.com
Access-Control-Allow-Headers: Authorization
Access-Control-Expose-Headers: X-Date
Access-Control-Allow-Credentials: true
大家可能也看出來了一個特點,與 CORS 請求相關的欄位,都以 Access-Control- 開頭。
如果跨域請求是被允許的,那麼響應頭部中是必須有 Access-Control-Allow-Origin 頭部的。它的值要麼是請求時 Origin 欄位的值,要麼是一個 *,表示接受任意域名的請求。
Access-Control-Allow-Credentials 是一個可選欄位,它的值是一個布林值,表示是否允許傳送Cookie。如果發起跨域請求時,設定了 withCredentials 標誌為 true,瀏覽器在發起跨域請求時,也會同時向伺服器傳送 cookie。如果伺服器端的響應中不存在 Access-Control-Allow-Credentials 頭部,瀏覽器就不會響應內容。
特別需要說明的是,如果請求端設定了 withCredentials ,Access-Control-Allow-Origin 的值就必須是具體的域名值,而不能設定為 *,否則瀏覽器也會丟擲跨域錯誤。
Access-Control-Expose-Headers 也是一個可選頭部。當進行跨域請求時,XMLHttpRequest 物件的 getResponseHeader()方法只能拿到 6 個基本響應欄位:
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
而如果開發者需要獲取其他響應頭部欄位,或者一些自定義響應頭部,伺服器就可以透過設定 Access-Control-Expose-Headers 頭部來指定發起端可訪問的響應頭部。
非簡單請求
非簡單請求往往是對伺服器有特殊要求的請求,比如請求方法為 PUT 或 DELETE,或者 Content-Type 欄位型別是 application/json。
對於非簡單請求的 CORS 請求,瀏覽器會在正式發起跨域請求之前,增加一次 HTTP 查詢請求,我們稱為預檢請求(preflight)。瀏覽器會先詢問伺服器,當前的域名是否在伺服器的許可名單之中,以及可以使用哪些 HTTP 請求方法和請求頭部欄位。只有得到肯定答覆,瀏覽器才會發出正式的跨域請求,否則就會報錯。
比方說我們使用程式碼發起一個跨域請求:
fetch('http://opentalk.upyun.com/data/', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CUSTOM-HEADER': '123'
}
})
瀏覽器會發現這是一個非簡單請求,它會自動傳送一個 OPTIONS 的預檢請求,其中核心內容有兩部分,Access-Control-Request-Method 表示後面的跨域請求需要用到的方法,Access-Control-Request-Headers 表示後面的跨域請求頭內會有該內容。
OPTIONS /data/ HTTP/1.1
Host: opentalk.upyun.com
Origin: http://www.upyun.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-MY-CUSTOM-HEADER, Content-Type
伺服器收到預檢請求後,檢查這些特殊的請求方法和頭自己能否接受,如果接受,會在響應頭部中包含如下資訊:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: X-Date, range, X-Custom-Header, Content-Type
Access-Control-Expose-Headers: X-Date, X-File, Content-type
......
上面的 HTTP 響應中,關鍵的是 Access-Control-Allow-Origin 欄位,* 表示同意任意跨源請求都可以請求資料。部分欄位我們在簡單請求中解釋過了,這裡挑幾個需要注意的頭部解釋一下。
- Access-Control-Allow-Methods,這是個不可缺少的欄位,它的值是逗號分隔的一個字串,表明伺服器支援的所有跨域請求的方法。
- Access-Control-Allow-Headers 欄位為一個逗號分隔的字串,表明伺服器支援的所有請求頭部資訊欄位,不限於瀏覽器在預檢中請求的欄位。
- Access-Control-Max-Age:該欄位可選,用來指定本次預檢請求的有效期,單位為秒。上面結果中,有效期是 1 天(86400 秒),在此期間,不用再發出另一條預檢請求。
又拍雲 CORS 配置
以上就是對 CORS 的一個簡單介紹。如果您使用了又拍雲的 CDN 或者雲端儲存服務, 在訪問中遇到跨域問題,是可以非常快速便捷的進行 CORS 配置的。
登陸服務控制檯,依次進入:服務管理 > 功能配置 > 訪問控制 > CORS 跨域共享,點選【管理】按鈕即可開始配置。如下圖所示:
相信您看完本篇文章,對配置介面的各個欄位都不再陌生啦。