系列文章:
- 【小哥哥, 跨域要不要了解下】JSONP
- 【小哥哥, 跨域要不要了解下】CORS 基礎篇
- 【小哥哥, 跨域要不要了解下】CORS 進階篇
- 【小哥哥, 跨域要不要了解下】NGINX 反向代理
- 【小哥哥, 跨域要不要了解下】ServerProxy
預檢請求的誕生
在前一篇文章結尾, 我們發現使用 CORS 方式實現跨域, 有時候會傳送兩個請求
一個 OPTIONS 一個正常請求
, 這個 OPTIONS 是個什麼鬼呢?
下面貼一段 MDN 的解釋
眾所周知, 後端 API 設計比較流行的正規化就是 restful(到 2018 年 12 月 8 日). 在 restful 中分別用不同的 HTTP METHOD 標識後端的 CURD, 對於使用這些可能會更新後端資料的 HTTP METHOD 發出的跨域請求, 瀏覽器要首先和伺服器商定一下當前的域名是不是有執行對應的 CURD 的許可權. 於是這個 OPTIONS 型別的 預檢請求
就誕生了. 那麼問題來了 可能對伺服器資料產生副作用的 HTTP 請求方法
是有那些咧? 不知道麼有關係, TIM 隊長為我們探探路 ?
簡單請求 VS 複雜請求
在 CORS 機制中, 把請求分為了 簡單請求
和 複雜請求
, 一個 HTTP 請求若想要讓自己成為一個簡單請求就要滿足以下條件:
- 首先, 請求方式的限制: 請求方式(method) 只能是
GET POST HEAD
三者中的一個 - 其次就是請求頭欄位的限制: 請求頭欄位必須包含在以下集合中, 包括:
Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width
. - 其次就是請求頭值的限制: 當請求頭中包含
Content-Type
的時候, 其值必須為text/plain multipart/form-data application/x-www-form-urlencoded(這個是 form 提交預設的 Content-Type)
三者中的一個.
綜上, 只要前端發出的請求滿足以上三個條件, 你發出的請求就是簡單請求. 那麼什麼事複雜請求呢? 答: 只要不是簡單請求就是複雜請求. 原本以為很難理清的概念, 居然只有三個條件搞定 ^_^.
再告訴大家一個祕密, 所有的簡單請求跨域訪問都是不會觸發預檢請求的喲. 那是複雜請求的專利...
預檢請求都幹了啥 ?
對於複雜請求發生跨域訪問前, 總是要通過預檢請求進行鑑權. 那麼鑑權的過程到底是啥麼樣子的呢? 這一步我們一起來研究一下.
- 首先, 開啟上一節的程式碼
- 分別執行
node ./be/cors/index.js
live-server ./fe/cors
啟動後端服務和前端的 web 容器. - 瀏覽器自動開啟後開啟控制檯, 切換到 Network tab 並重新整理瀏覽器. 不出意外的話, 看到的是這個樣子的
- 點選一下第一個 localhost 請求並檢視詳情 不難發現, 響應頭裡標註的幾個欄位, 就是我們的後端專案裡邊寫的幾個.
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.setHeader('Access-Control-Allow-Headers', 'token');
response.setHeader('Access-Control-Max-Age', 5);
複製程式碼
一一對應, 絕非偶然 ?. 那麼請求頭中標註的兩個又是什麼意思呢?
瀏覽器在接受到我們傳送的跨域請求的指令時, 會自動判斷我們的請求是否屬於跨域請求, 如果是的話便會發出預檢請求, 預檢請求的請求頭資訊也是瀏覽器根據我們的請求資訊自動新增的. 示例專案中, 因為我們的請求是 PUT
型別的, 所以在預檢請求的時候會新增 Access-Control-Allow-Methods: PUT
來諮詢伺服器自己是否可以向它傳送這種型別的請求. 同理, 由於我們的請求中有自定義請求頭 token
所以, 在預檢請求中, 瀏覽器要和伺服器做是否可以新增自定義請求頭的協商. 只有當瀏覽器和伺服器之間的預檢請求協商通過了, 瀏覽器才會繼續傳送真正的 AJAX
請求.
老闆說, 我不想看到多餘的請求
在工作中, 老闆往往是不懂技術的. 能看控制檯的老闆一般是高手了. 面對這種一個 api 發兩次請求的情況可能一個程式設計師笑笑也就過去了, 但是老闆就不這麼認為了, 一個介面他就要一次請求. 你要把 圈圈的圈
跨域文章推薦給老闆, 讓小哥哥也瞭解下? 估計你會被 fire 掉. 那腫麼辦呢?
面對這種情況, 有兩種解決方案.
- 第一種, 可以和後端小哥哥商量一下. 把介面改成簡單請求, 預檢請求的問題就迎刃而解了.
- 然而, 有時候寫好的程式碼誰都不願意去改. 後端小哥哥不聽話. 這種情況下
Access-Control-Max-Age
就派上用場了. 這個響應頭的意思是預檢請求的有效期. 在指定時間內再次跨域訪問介面, 是不需要預檢請求的, 單位是秒
. 如果我們把有效時間寫的非常的長, 那麼四不四看上去就像刪除了預檢請求了呢 ^_^. - 附加情況, 你老闆不懂技術瞎 J8 指揮. 小爺我不幹了. 當然這種處理方案比較不推薦.
PS: 使用 Access-Control-Max-Age
機制和快取類似, 所以給老闆演示的時候千萬不要清理快取. 不要勾選 Network 下的 disable cache
. 不說啦, 都是淚...
我們們不能允許所有的人都訪問呀
通過 Access-Control-Allow-Origin
, 可以在後端設定可以跨域訪問我們的域名列表, *
代表所有的域名都可以跨域訪問我們的後端, 這樣其實是有隱患的. 為了安全起見, 我們把可以跨域訪問的域名限制為我們已知的域名. 老規矩.
後端程式碼
// 修改一行程式碼, 一定要新增協議喲
response.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
複製程式碼
修改以後瀏覽器訪問 http://127.0.0.1:8080
如果想要開放多個域名的跨域訪問咋辦咧?
如果我們有多個業務域名需要跨域訪問同一個伺服器, 可以把允許的域名列表儲存到一個陣列裡. 接到請求之後先判斷當前請求域名是否在我們允許的域名列表裡, 如果在的話直接新增到響應頭 Access-Control-Allow-Origin
下.
後端程式碼
const http = require('http');
const PORT = 8888;
// 協議名必填, 如果同時存在 http 和 https 就寫兩條s
const allowOrigin = ['http://127.0.0.1:8080', 'https://www.baidu.com'];
// 建立一個 http 服務
const server = http.createServer((request, response) => {
const { headers: { origin } } = request;
if (allowOrigin.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.setHeader('Access-Control-Allow-Headers', 'token');
response.setHeader('Access-Control-Max-Age', 5);
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 啟動服務, 監聽埠
server.listen(PORT, () => {
console.log('服務啟動成功, 正在監聽: ', PORT);
});
複製程式碼
此時程式碼, 首先訪問http://127.0.0.1:8080
響應結果成功列印, 沒有任何問題.其次訪問 www.baidu.com, 開啟控制檯, 執行
xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:8888')
xhr.onreadystatechange = () => {
xhr.status === 200 && xhr.readyState === 4 && console.log(xhr.responseText)
}
xhr.send()
複製程式碼
沒有任何報錯, 返回結果成功列印. 成功...
你的請求怎麼沒有攜帶 Cookie
一般情況下, 前端發出的跨域的 ajax OR fetch 請求是不會攜帶 Cookie 的. 但是, 後端小哥哥還要. 咋弄咧? 加上唄.
前端程式碼:
// 在 xhr.send 之前新增這一行
xhr.withCredentials = true;
複製程式碼
新增完以後, 重新整理瀏覽器.
對於這個報錯, 不知道你有沒有啥好說的, 反正我是沒啥話了...
後端程式碼:
const http = require('http');
const PORT = 8888;
// 協議名必填, 如果同時存在 http 和 https 就寫兩條s
const allowOrigin = ['http://127.0.0.1:8080', 'http://localhost:8080', 'https://www.baidu.com'];
// 建立一個 http 服務
const server = http.createServer((request, response) => {
const { method, headers: { origin, cookie } } = request;
if (allowOrigin.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
response.setHeader('Access-Control-Allow-Methods', 'PUT');
// 允許前端請求攜帶 Cookie
response.setHeader('Access-Control-Allow-Credentials', true);
response.setHeader('Access-Control-Allow-Headers', 'token');
if (method === 'OPTIONS') {
console.log('預檢請求');
} else if (!cookie) {
// 如果不存在 Cookie 就設定 Cookie
response.setHeader('Set-Cookie', 'quanquan=fe');
}
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 啟動服務, 監聽埠
server.listen(PORT, () => {
console.log('服務啟動成功, 正在監聽: ', PORT);
});
複製程式碼
此時程式碼, 再次到瀏覽器看一下.
Cookie 中多了一條
請求中攜帶了 Cookie
通過下邊的動圖可以看出, 我們前後端 Cookie 傳遞非常的通暢.
我在響應頭上給你返回了 Token, 你取出來放在請求頭上
工作中常常遇到後端把一些標識放在響應頭上返回給前端的 case, 比如使用者登入, 後端返回使用者的唯一標識放在響應頭上. 需要前端獲取, 後續的請求都需要把這個標識放在請求頭, 用於驗證使用者的身份.
我們首先修改後端程式碼:
// 在 response.end() 前新增這一行
response.setHeader('token', 'quanquan');
複製程式碼
修改前端程式碼:
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
// 列印響應資料時同時列印所有響應頭
console.log(xhr.getAllResponseHeaders())
}
}
複製程式碼
修改完成後程式碼, 瀏覽器看一下.
console.log 列印出了空行
但是在 Network Tab 下後端確實返回了響應頭 token 欄位. 懵逼了...
原來, Access-Control-
系列還有一個響應頭 Access-Control-Expose-Headers
, 我們在後端程式碼 response.end(...)
之前加上 response.setHeader('Access-Control-Expose-Headers', 'token');
再次會瀏覽器檢視
成功了 ?.
預檢請求不返回內容把
我們的響應結果本來應該是在正式的請求中才需要返回的, 但是我們看下預檢請求的返回詳情發現
預檢請求只是瀏覽器層面的解析, 前端程式碼根本拿不到. 這裡的內容僅僅是浪費頻寬和使用者的流量. 所以我們改造一下.預檢請求不再返回內容.
後端程式碼:
const http = require('http');
const PORT = 8888;
// 協議名必填, 如果同時存在 http 和 https 就寫兩條s
const allowOrigin = ['http://127.0.0.1:8080', 'http://localhost:8080', 'https://www.baidu.com'];
// 建立一個 http 服務
const server = http.createServer((request, response) => {
const { method, headers: { origin, cookie } } = request;
if (allowOrigin.includes(origin)) {
response.setHeader('Access-Control-Allow-Origin', origin);
}
response.setHeader('Access-Control-Allow-Methods', 'PUT');
response.setHeader('Access-Control-Allow-Credentials', true);
response.setHeader('Access-Control-Allow-Headers', 'token');
response.setHeader('Access-Control-Expose-Headers', 'token');
response.setHeader('token', 'quanquan');
if (method === 'OPTIONS') {
response.writeHead(204);
response.end('');
} else if (!cookie) {
response.setHeader('Set-Cookie', 'quanquan=fe');
}
response.end("{name: 'quanquan', friend: 'guiling'}");
});
// 啟動服務, 監聽埠
server.listen(PORT, () => {
console.log('服務啟動成功, 正在監聽: ', PORT);
});
複製程式碼
此時程式碼, 驗證, 就不驗證了吧. 好使 ?
下基預告: 前兩種跨域方案就算是講完了, 不少小夥伴吐槽, jsonp 太老, cors 太麻煩.... 那麼下一節我們嘗試一下 反向代理
, See you