CORS原理及實現

CORS跨域的原理

跨域資源共享(CORS)是一種機制胶逢,是W3C標準同云。它允許瀏覽器向跨源服務器咱扣,發(fā)出XMLHttpRequestFetch請求。并且整個CORS通信過程都是瀏覽器自動完成的企量,不需要用戶參與测萎。

而使用這種跨域資源共享的前提是,瀏覽器必須支持這個功能届巩,并且服務器端也必須同意這種"跨域"請求硅瞧。因此實現CORS的關鍵是服務器需要服務器。通常是有以下幾個配置:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Credentials
  • Access-Control-Max-Age

具體可看:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#Preflighted_requests

過程分析:

  • 瀏覽器先根據同源策略對前端頁面和后臺交互地址做匹配恕汇,若同源腕唧,則直接發(fā)送數據請求;若不同源瘾英,則發(fā)送跨域請求枣接。

  • 服務器收到瀏覽器跨域請求后,根據自身配置返回對應文件頭缺谴。若未配置過任何允許跨域但惶,則文件頭里不包含 Access-Control-Allow-origin 字段,若配置過域名湿蛔,則返回 Access-Control-Allow-origin + 對應配置規(guī)則里的域名的方式膀曾。

  • 瀏覽器根據接受到的 響應頭里的 Access-Control-Allow-origin 字段做匹配,若無該字段煌集,說明不允許跨域妓肢,從而拋出一個錯誤;若有該字段苫纤,則對字段內容和當前域名做比對碉钠,如果同源,則說明可以跨域卷拘,瀏覽器接受該響應喊废;若不同源,則說明該域名不可跨域栗弟,瀏覽器不接受該響應污筷,并拋出一個錯誤。

另外在CORS中有簡單請求非簡單請求乍赫,簡單請求是不會觸發(fā)CORS的預檢請求的瓣蛀,而非簡單請求會。

“需預檢的請求”要求必須首先使用 OPTIONS 方法發(fā)起一個預檢請求到服務器雷厂,以獲知服務器是否允許該實際請求惋增。"預檢請求“的使用,可以避免跨域請求對服務器的用戶數據產生未預期的影響改鲫。

CORS的哪些是簡單請求诈皿?

簡單請求不會觸發(fā)CORS的預檢請求林束,若請求滿足所有下述條件,則該請求可視為“簡單請求”:

簡單回答

  • 只能使用GET稽亏、HEAD壶冒、POST方法。使用POST方法向服務器發(fā)送數據時截歉,Content-Type只能使用application/x-www-form-urlencoded胖腾、multipart/form-datatext/plain編碼格式。
  • 請求時不能使用自定義的HTTP Headers

詳細回答

  • (一) 使用下列方法之一

    • GET
    • HEAD
    • POST
  • (二) 只能設置以下集合中的請求頭

    • 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
  • 請求中的任意XMLHttpRequestUpload 對象均沒有注冊任何事件監(jiān)聽器怎披;XMLHttpRequestUpload 對象可以使用 XMLHttpRequest.upload 屬性訪問胸嘁。

  • 請求中沒有使用 ReadableStream 對象。

除了上面這些請求外凉逛,都是非簡單請求。

CORS的預檢請求具體是怎樣的群井?

若是跨域的非簡單請求的話状飞,瀏覽器會首先向服務器發(fā)送一個預檢請求,以獲知服務器是否允許該實際請求书斜。

整個過程大概是:

  • 瀏覽器給服務器發(fā)送一個OPTIONS方法的請求诬辈,該請求會攜帶下面兩個首部字段:
    • Access-Control-Request-Method: 實際請求要用到的方法
    • Access-Control-Request-Headers: 實際請求會攜帶哪些首部字段
  • 若是服務器接受后續(xù)請求,則這次預請求的響應體中會攜帶下面的一些字段:
    • Access-Control-Allow-Methods: 服務器允許使用的方法
    • Access-Control-Allow-Origin: 服務器允許訪問的域名
    • Access-Control-Allow-Headers: 服務器允許的首部字段
    • Access-Control-Max-Age: 該響應的有效時間(s),在有效時間內瀏覽器無需再為同一個請求發(fā)送預檢請求
  • 預檢請求完畢之后荐吉,再發(fā)送實際請求

這里有兩點要注意:

一:

Access-Control-Request-Method沒有s

Access-Control-Allow-Methodss

二:

關于Access-Control-Max-Age焙糟,瀏覽器自身也有維護一個最大有效時間,如果該首部字段的值超過了最大有效時間样屠,將不會生效穿撮,而是以最大有效時間為主。

CORS簡單請求的案例

還是在原本JSONP的那個案例上痪欲。

我在根目錄下新建了一個文件夾cors悦穿,并往里面添加了一個index.html文件:

/cors/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CORS</title>
</head>
<body>
  <button id="getName">獲取name</button>
  <script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
  <script>
    getName.onclick = () => {
      // 簡單請求
      axios.get("http://127.0.0.1:8080/api/corsname");
    }
  </script>
</body>
</html>

為了后面也方便調試,用node簡單寫了一個前端的本地服務和后端的本地服務业踢。

在根目錄下新建client.js文件栗柒,并寫入:

./client.js:

const Koa = require('koa');
const fs = require('fs');
const app = new Koa();

app.use(async (ctx) => {
  if (ctx.method === 'GET' && ctx.path === '/') {
    ctx.body = fs.readFileSync('./index.html').toString();
  }
  if (ctx.method === 'GET' && ctx.path === '/cors') {
    ctx.body = fs.readFileSync('./cors/index.html').toString();
  }
})
console.log('client 8000...')
app.listen(8000);

在根目錄下新建index.html文件,并寫入:

./index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Client</title>
</head>
<body>
  <ul>
    <li>
      <a href="/cors">CORS跨域</a>
    </li>
  </ul>
</body>
</html>

(以上:實現了一個簡單的前端路由效果)

在根目錄下新建server.js文件知举,并寫入:

./server.js:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", ctx.header.origin);
  // ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  // ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
  ctx.set(
    "Access-Control-Allow-Headers", 
    "Origin, X-Requested-With, Content-Type, Access, cc"
  )
  if (ctx.method === 'OPTIONS') {
    ctx.status = 204;
    return;
  }
  await next();
  if (ctx.path === '/api/corsname') {
    ctx.body = {
      data: 'LinDaiDai'
    }
    return;
  }
})
console.log('server 8080...')
app.listen(8080);

并給package.json中配置兩個啟動指令:

package.json:

{
  "scripts": {
    "client": "node ./client.js",
    "server": "node ./server.js"
    }
}

OK??瞬沦,來分別啟動一下npm run clientnpm run server

并打開頁面的127.0.0.1:8000/cors(或者打開127.0.0.1:8000然后點擊CORS這個a標簽)

點擊獲取name按鈕,可以看到能夠正常獲取到本地服務器的數據了雇锡。

CORS非簡單請求的案例

接著讓我們來改造一下./cors/index.html中的按鈕點擊請求逛钻,讓它變成一個非簡單請求:

./cors/index.html:

getName.onclick = () => {
  // 簡單請求
  // axios.get("http://127.0.0.1:8080/api/corsname");

  // 非簡單請求
  axios.get('http://127.0.0.1:8080/api/corsname', {
    headers: {
      cc: 'lindaidai'
    }
  })
}

此時,打開頁面點擊按鈕會發(fā)現發(fā)送了兩次corsname的請求:

(一)預檢請求:

cors1.png

(二)實際請求:

cors2.png

CORS附帶身份憑證的案例

對于跨域 XMLHttpRequestFetch 請求遮糖,瀏覽器不會發(fā)送身份憑證信息绣的。如果要發(fā)送憑證信息,需要設置 XMLHttpRequest的某個特殊標志位。

例如我們想要在跨域請求中帶上cookie屡江,需要滿足3個條件:

  • web(瀏覽器)請求設置withCredentialstrue
  • 服務器設置首部字段Access-Control-Allow-Credentialstrue
  • 服務器的Access-Control-Allow-Origin不能為*

所以為了模擬這個效果芭概,讓我們來寫一個小小的登錄+獲取數據的功能吧。

首先對于web端惩嘉,我新增了一個登錄按鈕罢洲,并且配置了一下axios

./cors/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CORS</title>
</head>
<body>
  <button id="getName">獲取name</button>
  <button id="login">登錄</button>
  <script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
  <script>
    axios.defaults.withCredentials = true;
    axios.defaults.baseURL = 'http://127.0.0.1:8080'
    login.onclick = () => {
      axios.post('/api/login')
    }
    getName.onclick = () => {
      axios.get('/api/corsname').then(res => console.log(res.data))
    }
  </script>
</body>
</html>

接著為了更方便的模擬后臺請求,我需要在項目中安裝兩個中間件:

cnpm i --save-dev koa-router koa-body

接著修改一下server.js的后臺配置:

./server.js:

const Koa = require("koa");
const router = require("koa-router")();
const koaBody = require("koa-body");
const app = new Koa();
const TOKEN = "112233"; // 模擬寫死一個token

app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", ctx.header.origin);
  // ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
  ctx.set(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Access, cc"
  );
  ctx.set("Access-Control-Allow-Credentials", true); // 這步很重要
  if (ctx.method === "OPTIONS") {
    ctx.status = 204;
    return;
  }
  await next();
});
app.use(async (ctx, next) => {
  // 若是登錄接口則跳過后面的token驗證
  if (ctx.path === "/api/login") {
    await next();
    return;
  }
  // 對所有非登錄的請求驗證token
  const cookies = ctx.cookies.get("token");
  console.log(cookies);
  if (cookies && cookies === TOKEN) {
    await next();
    return;
  }
  ctx.body = {
    code: 401,
    msg: "權限錯誤",
  };
  return;
});
// 如果不加multipart:true ctx.request.body會獲取不到值
app.use(koaBody({ multipart: true }));

router.get("/api/corsname", async (ctx) => {
  ctx.body = {
    data: "LinDaiDai",
  };
});

router.post("/api/login", async (ctx) => {
  ctx.cookies.set("token", TOKEN, {
    expires: new Date(+new Date() + 1000 * 60 * 60 * 24 * 7),
  });
  ctx.body = {
    msg: "成功",
    code: 0,
  };
});

app.use(router.routes());
console.log("server 8080...");
app.listen(8080);

現在讓我們重啟一下服務文黎,然后打開頁面看看效果:

(一)點擊登錄:

cors3.png

(二)點擊獲取name:

cors4.png

(三)查看cookie:

cors5.png

如何減少CORS預請求的次數惹苗?

方案一:發(fā)出簡單請求(這不是廢話嗎...)

方案二:服務端設置Access-Control-Max-Age字段,在有效時間內瀏覽器無需再為同一個請求發(fā)送預檢請求耸峭。但是它有局限性:只能為同一個請求緩存桩蓉,無法針對整個域或者模糊匹配 URL 做緩存。

參考文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末劳闹,一起剝皮案震驚了整個濱河市院究,隨后出現的幾起案子,更是在濱河造成了極大的恐慌本涕,老刑警劉巖业汰,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異菩颖,居然都是意外死亡样漆,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門晦闰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來放祟,“玉大人,你說我怎么就攤上這事鹅髓∥韪停” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵窿冯,是天一觀的道長骗奖。 經常有香客問我,道長醒串,這世上最難降的妖魔是什么执桌? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮芜赌,結果婚禮上仰挣,老公的妹妹穿的比我還像新娘。我一直安慰自己缠沈,他們只是感情好膘壶,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布错蝴。 她就那樣靜靜地躺著,像睡著了一般颓芭。 火紅的嫁衣襯著肌膚如雪蚪腋。 梳的紋絲不亂的頭發(fā)上堂污,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天凰慈,我揣著相機與錄音引瀑,去河邊找鬼。 笑死州藕,一個胖子當著我的面吹牛束世,可吹牛的內容都是我干的。 我是一名探鬼主播床玻,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼毁涉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了笨枯?” 一聲冷哼從身側響起薪丁,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎馅精,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體粱檀,經...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡洲敢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了茄蚯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片压彭。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖渗常,靈堂內的尸體忽然破棺而出壮不,到底是詐尸還是另有隱情,我是刑警寧澤皱碘,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布询一,位于F島的核電站,受9級特大地震影響癌椿,放射性物質發(fā)生泄漏健蕊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一踢俄、第九天 我趴在偏房一處隱蔽的房頂上張望缩功。 院中可真熱鬧,春花似錦都办、人聲如沸嫡锌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽势木。三九已至蛛倦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間跟压,已是汗流浹背胰蝠。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留震蒋,地道東北人茸塞。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像查剖,于是被迫代替她去往敵國和親钾虐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354