【小哥哥, 跨域要不要了解下】CORS 進階篇

系列文章:前端

預檢請求的誕生

前一篇文章結尾, 咱們發現使用 CORS 方式實現跨域, 有時候會發送兩個請求 一個 OPTIONS 一個正常請求, 這個 OPTIONS 是個什麼鬼呢?node

下面貼一段 MDN 的解釋 git

2018-12-08-09-17-13

衆所周知, 後端 API 設計比較流行的範式就是 restful(到 2018 年 12 月 8 日). 在 restful 中分別用不一樣的 HTTP METHOD 標識後端的 CURD, 對於使用這些可能會更新後端數據的 HTTP METHOD 發出的跨域請求, 瀏覽器要首先和服務器商定一下當前的域名是否是有執行對應的 CURD 的權限. 因而這個 OPTIONS 類型的 預檢請求 就誕生了. 那麼問題來了 可能對服務器數據產生反作用的 HTTP 請求方法 是有那些咧? 不知道麼有關係, TIM 隊長爲咱們探探路 😄程序員

2018-12-08-09-32-33

簡單請求 VS 複雜請求

在 CORS 機制中, 把請求分爲了 簡單請求複雜請求, 一個 HTTP 請求若想要讓本身成爲一個簡單請求就要知足如下條件:github

  • 首先, 請求方式的限制: 請求方式(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) 三者中的一個.

綜上, 只要前端發出的請求知足以上三個條件, 你發出的請求就是簡單請求. 那麼什麼事複雜請求呢? 答: 只要不是簡單請求就是複雜請求. 本來覺得很難理清的概念, 竟然只有三個條件搞定 ^_^.web

2018-12-08-09-58-34

再告訴你們一個祕密, 全部的簡單請求跨域訪問都是不會觸發預檢請求的喲. 那是複雜請求的專利...ajax

預檢請求都幹了啥 😳

對於複雜請求發生跨域訪問前, 老是要經過預檢請求進行鑑權. 那麼鑑權的過程究竟是啥麼樣子的呢? 這一步咱們一塊兒來研究一下.json

  • 首先, 打開上一節的代碼
  • 分別執行 node ./be/cors/index.js live-server ./fe/cors 啓動後端服務和前端的 web 容器.
  • 瀏覽器自動打開後打開控制檯, 切換到 Network tab 並刷新瀏覽器. 不出意外的話, 看到的是這個樣子的
    2018-12-08-10-09-38
  • 點擊一下第一個 localhost 請求並查看詳情
    2018-12-08-10-22-25
    不難發現, 響應頭裏標註的幾個字段, 就是咱們的後端項目裏邊寫的幾個.
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

老闆說, 我不想看到多餘的請求

在工做中, 老闆每每是不懂技術的. 能看控制檯的老闆通常是高手了. 面對這種一個 api 發兩次請求的狀況可能一個程序員笑笑也就過去了, 可是老闆就不這麼認爲了, 一個接口他就要一次請求. 你要把 圈圈的圈 跨域文章推薦給老闆, 讓小哥哥也瞭解下? 估計你會被 fire 掉. 那腫麼辦呢?

2018-12-08-10-42-18

面對這種狀況, 有兩種解決方案.

  • 第一種, 能夠和後端小哥哥商量一下. 把接口改爲簡單請求, 預檢請求的問題就迎刃而解了.
  • 然而, 有時候寫好的代碼誰都不肯意去改. 後端小哥哥不聽話. 這種狀況下 Access-Control-Max-Age 就派上用場了. 這個響應頭的意思是預檢請求的有效期. 在指定時間內再次跨域訪問接口, 是不須要預檢請求的, 單位是 . 若是咱們把有效時間寫的很是的長, 那麼四不四看上去就像刪除了預檢請求了呢 ^_^.
  • 附加狀況, 你老闆不懂技術瞎 J8 指揮. 小爺我不幹了. 固然這種處理方案比較不推薦.

PS: 使用 Access-Control-Max-Age 機制和緩存相似, 因此給老闆演示的時候千萬不要清理緩存. 不要勾選 Network 下的 disable cache. 不說啦, 都是淚...

2018-12-08-11-13-21

我們不能容許全部的人都訪問呀

經過 Access-Control-Allow-Origin, 能夠在後端設置能夠跨域訪問咱們的域名列表, * 表明全部的域名均可以跨域訪問咱們的後端, 這樣實際上是有隱患的. 爲了安全起見, 咱們把能夠跨域訪問的域名限制爲咱們已知的域名. 老規矩.

2018-12-08-11-24-04

後端代碼

// 修改一行代碼, 必定要添加協議喲
response.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
複製代碼

修改之後瀏覽器訪問 http://127.0.0.1:8080

2018-12-08-11-27-57

若是想要開放多個域名的跨域訪問咋辦咧?

若是咱們有多個業務域名須要跨域訪問同一個服務器, 能夠把容許的域名列表保存到一個數組裏. 接到請求以後先判斷當前請求域名是否在咱們容許的域名列表裏, 若是在的話直接添加到響應頭 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

2018-12-08-11-46-03
響應結果成功打印, 沒有任何問題.

其次訪問 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()
複製代碼

2018-12-08-11-54-13

沒有任何報錯, 返回結果成功打印. 成功...

你的請求怎麼沒有攜帶 Cookie

通常狀況下, 前端發出的跨域的 ajax OR fetch 請求是不會攜帶 Cookie 的. 可是, 後端小哥哥還要. 咋弄咧? 加上唄.

2018-12-08-12-05-05

前端代碼:

// 在 xhr.send 以前添加這一行
xhr.withCredentials = true;
複製代碼

添加完之後, 刷新瀏覽器.

2018-12-08-12-07-24

對於這個報錯, 不知道你有沒有啥好說的, 反正我是沒啥話了...

後端代碼:

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 中多了一條

2018-12-08-12-37-50

請求中攜帶了 Cookie

2018-12-08-12-37-26

經過下邊的動圖能夠看出, 咱們先後端 Cookie 傳遞很是的通暢.

cookie-2134567

我在響應頭上給你返回了 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 打印出了空行

2018-12-08-13-26-21

可是在 Network Tab 下後端確實返回了響應頭 token 字段. 懵逼了...

2018-12-08-13-28-41

原來, Access-Control- 系列還有一個響應頭 Access-Control-Expose-Headers, 咱們在後端代碼 response.end(...) 以前加上 response.setHeader('Access-Control-Expose-Headers', 'token');再次會瀏覽器查看

2018-12-08-13-31-40

成功了 😄.

預檢請求不返回內容把

咱們的響應結果原本應該是在正式的請求中才須要返回的, 可是咱們看下預檢請求的返回詳情發現

2018-12-08-13-34-09

預檢請求只是瀏覽器層面的解析, 前端代碼根本拿不到. 這裏的內容僅僅是浪費帶寬和用戶的流量. 因此咱們改造一下.預檢請求再也不返回內容.

後端代碼:

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

相關文章
相關標籤/搜索