CORS跨域的原理
跨域資源共享(CORS
)是一種機制胶逢,是W3C標準同云。它允許瀏覽器向跨源服務器咱扣,發(fā)出XMLHttpRequest
或Fetch
請求。并且整個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-data
或text/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-Methods
有s
二:
關于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 client
和npm 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
的請求:
(一)預檢請求:
(二)實際請求:
CORS附帶身份憑證的案例
對于跨域 XMLHttpRequest
或 Fetch 請求遮糖,瀏覽器不會發(fā)送身份憑證信息绣的。如果要發(fā)送憑證信息,需要設置 XMLHttpRequest
的某個特殊標志位。
例如我們想要在跨域請求中帶上cookie
屡江,需要滿足3個條件:
- web(瀏覽器)請求設置
withCredentials
為true
- 服務器設置首部字段
Access-Control-Allow-Credentials
為true
- 服務器的
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);
現在讓我們重啟一下服務文黎,然后打開頁面看看效果:
(一)點擊登錄:
(二)點擊獲取name:
(三)查看cookie:
如何減少CORS預請求的次數惹苗?
方案一:發(fā)出簡單請求(這不是廢話嗎...)
方案二:服務端設置Access-Control-Max-Age
字段,在有效時間內瀏覽器無需再為同一個請求發(fā)送預檢請求耸峭。但是它有局限性:只能為同一個請求緩存桩蓉,無法針對整個域或者模糊匹配 URL 做緩存。