嗯。又來了,又說到跨域了,這是一個老生常談的話題,以前我覺得這種基礎文章沒有什么好寫的,會想著你去了解底層啊,不是很簡單嗎。但是最近在開發一個 「vscode 插件」 發現,當你剛入門一樣東西的時候,你不會想這么多,因為你對他不熟悉,當你遇到不會的東西,你就是想先找到解決方案,然后通過這個解決方案再去深入理解。就比如跨域,新人或者剛接觸的人對它并不是那么熟悉,所以說列出一些自己積累的方案,以及一些常用的場景來給他人帶來一些解決問題的思路,這件事是有意義的。(寫完之后還發現真香。以后忘了還能回來看看)
其實現在的環境對于剛入門的前端來說,非常的不友好,一方面吧,很多剛新人沒有經歷過工具的變更時代,另一方面框架的迭代更新速度很快。在以前你可能掌握幾種常見的手法就好了。但是現在 webpack-dev-server
、vue-cli
、parcel
,這些腳手架都進行了一層封裝,對于熟悉的人可能很簡單,但是對于還未入門的人來說,簡直就是一個黑盒,雖然用起來很方便,但是某一天遇到了問題,你對它不熟悉,你就會不知道所錯。(但是別慌,主流 cli 的跨域方式我都會講到)
講著講著有點偏離方向,可能我講的也并不一定是正確的。下面切入正題。
本文會以 「「What-How-Why」」 的方式來進行講解。而在在 How (如何解決跨域,將會提供標題的 11 種方案。)
「重要的說明: 在文中,web 端地址為 localhost:8000 服務端地址為 localhost:8080,這一點希望你能記住,會貫穿全文,你也可以把此處的兩端的地址代入你自己的地址?!?/strong>
cors以下所有代碼均在 https://github.com/hua1995116/node-demo/tree/master/node-cors
image-20200413192431636
跨域問題其實就是瀏覽器的同源策略所導致的。
?「同源策略」是一個重要的安全策略,它用于限制一個origin的文檔或者它加載的腳本如何能與另一個源的資源進行交互。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介。
--來源 MDN
?
當跨域時會收到以下錯誤
image-20200413205559124
那么如何才算是同源呢?先來看看 url 的組成部分
http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
image-20200412171942421
只有當
「protocol(協議)、domain(域名)、port(端口)三者一致?!?/strong>
「protocol(協議)、domain(域名)、port(端口)三者一致?!?/strong>
「protocol(協議)、domain(域名)、port(端口)三者一致?!?/strong>
才是同源。
以下協議、域名、端口一致。
http://www.example.com:80/a.js
http://www.example.com:80/b.js
以下這種看上去再相似也沒有用,都不是同源。
http://www.example.com:8080
http://www2.example.com:80
在這里注意一下啊,這里是為了突出端口的區別才寫上端口。在默認情況下 http 可以省略端口 80, https 省略 443。這別到時候鬧笑話了,你和我說 http://www.example.com:80 和 http://www.example.com 不是同源,他倆是一個東西。
http://www.example.com:80 === http://www.example.com
https://www.example.com:443 === https://www.example.com
唔,還是要說明一下。
跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的 Web 應用被準許訪問來自不同源服務器上的指定的資源。當一個資源從與該資源本身所在的服務器「不同的域、協議或端口」請求一個資源時,資源會發起一個「跨域 HTTP 請求」。
而在 cors 中會有 簡單請求
和 復雜請求
的概念。
「瀏覽器支持情況」
當你使用 IE<=9, Opera<12, or Firefox<3.5 或者更加老的瀏覽器,這個時候請使用 JSONP 。
不會觸發 CORS 預檢請求。這樣的請求為“簡單請求”,請注意,該術語并不屬于 Fetch (其中定義了 CORS)規范。若請求滿足所有下述條件,則該請求可視為“簡單請求”:
情況一: 使用以下方法(意思就是以下請求意外的都是非簡單請求)
GET
HEAD
POST
情況二: 人為設置以下集合外的請求頭
Accept
Accept-Language
Content-Language
Content-Type
(需要注意額外的限制)DPR
Downlink
Save-Data
Viewport-Width
Width
情況三:Content-Type
的值僅限于下列三者之一:(例如 application/json 為非簡單請求)
text/plain
multipart/form-data
application/x-www-form-urlencoded
情況四:
請求中的任意XMLHttpRequestUpload
對象均沒有注冊任何事件監聽器;XMLHttpRequestUpload
對象可以使用 XMLHttpRequest.upload
屬性訪問。
情況五:
請求中沒有使用 ReadableStream
對象。
除以上情況外的。
我們來看下后端部分的解決方案。Node
中 CORS
的解決代碼.
app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
ctx.set("Access-Control-Allow-Credentials", true);
ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
ctx.set(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, cc"
);
if (ctx.method === "OPTIONS") {
ctx.status = 204;
return;
}
await next();
});
為了方便也可以直接使用中間件
const cors = require("koa-cors");
app.use(cors());
想要傳遞 cookie
需要滿足 3 個條件
1.web 請求設置withCredentials
這里默認情況下在跨域請求,瀏覽器是不帶 cookie 的。但是我們可以通過設置 withCredentials
來進行傳遞 cookie
.
// 原生 xml 的設置方式
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// axios 設置方式
axios.defaults.withCredentials = true;
2.Access-Control-Allow-Credentials
為 true
3.Access-Control-Allow-Origin
為非 *
這里請求的方式,在 chrome
中是能看到返回值的,但是只要不滿足以上其一,瀏覽器會報錯,獲取不到返回值。
image-20200412201424024
Access to XMLHttpRequest at 'http://127.0.0.1:8080/api/corslist' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
image-20200412201411481
Access to XMLHttpRequest at 'http://127.0.0.1:8080/api/corslist' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
image-20200412201530087
分別演示一下前端部分 簡單請求
和 非簡單請求
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.get("http://127.0.0.1:8080/api/corslist");
</script>
這里我們加入了一個非集合內的 header
頭 cc
來達到非簡單請求的目的。
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.get("http://127.0.0.1:8080/api/corslist", { header: { cc: "xxx" } });
</script>
image-20200412201158778
image-20200412195829232
1、 在新版的 chrome 中,如果你發送了復雜請求,你卻看不到 options
請求??梢栽谶@里設置 chrome://flags/#out-of-blink-cors
設置成 disbale
,重啟瀏覽器。對于非簡單請求就能看到 options
請求了。
2、 一般情況下后端接口是不會開啟這個跨域頭的,除非是一些與用戶無關的不太重要的接口。
代理的思路為,利用服務端請求不會跨域的特性,讓接口和當前站點同域。
「代理前」
image-20200412202320482
「代理后」
image-20200412202358759
這樣,所有的資源以及請求都在一個域名下了。
在webpack
中可以配置proxy
來快速獲得接口代理的能力。
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
index: "./index.js"
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "webpack.html"
})
]
};
修改前端接口請求方式,改為不帶域名。(因為現在是同域請求了)
<button id="getlist">獲取列表</button>
<button id="login">登錄</button>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.defaults.withCredentials = true;
getlist.onclick = () => {
axios.get("/api/corslist").then(res => {
console.log(res.data);
});
};
login.onclick = () => {
axios.post("/api/login");
};
</script>
// config/index.js
...
proxyTable: {
'/api': {
target: 'http://localhost:8080',
}
},
...
// vue.config.js 如果沒有就新建
module.exports = {
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
}
};
// .proxyrc
{
"/api": {
"target": "http://localhost:8080"
}
}
看到這里,心里一句 xxx 就會脫口而出,一會寫配置文件,一會 proxyTable ,一會 proxy,怎么這么多的形式?學不動了學不動了。。。不過也不用慌,還是有方法的。以上所有配置都是有著共同的底層包 http-proxy-middleware .里面需要用到的各種 websocket
,rewrite
等功能,直接看這個庫的配置就可以了。關于 http-proxy-middleware 這個庫的原理可以看我這篇文章 https://github.com/hua1995116/proxy 當然了。。。對于配置的位置入口還是非統一的。教一個搜索的技巧吧,上面配置寫哪里都不用記的,想要哪個框架的 直接 google 搜索 xxx proxy 就行了。
例如 vue-cli 2 proxy 、 webpack proxy 等等....基本會搜到有官網的配置答案,通用且 nice。
cors-anywhere
「服務端」
// Listen on a specific host via the HOST environment variable
var host = process.env.HOST || "0.0.0.0";
// Listen on a specific port via the PORT environment variable
var port = process.env.PORT || 7777;
var cors_proxy = require("cors-anywhere");
cors_proxy
.createServer({
originWhitelist: [], // Allow all origins
requireHeader: ["origin", "x-requested-with"],
removeHeaders: ["cookie", "cookie2"]
})
.listen(port, host, function() {
console.log("Running CORS Anywhere on " + host + ":" + port);
});
「前端代碼」
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.defaults.withCredentials = true;
getlist.onclick = () => {
axios
.get("http://127.0.0.1:7777/http://127.0.0.1:8080/api/corslist")
.then(res => {
console.log(res.data);
});
};
login.onclick = () => {
axios.post("http://127.0.0.1:7777/http://127.0.0.1:8080/api/login");
};
</script>
「效果展示」
image-20200413161243734
「缺點」
無法專遞 cookie,原因是因為這個是一個代理庫,作者覺得中間傳遞 cookie 太不安全了。https://github.com/Rob--W/cors-anywhere/issues/208#issuecomment-575254153
這是一個測試、開發的神器。介紹與使用
利用 charles 進行跨域,本質就是請求的攔截與代理。
在 tools/map remote
中設置代理
image-20200412232733437
image-20200412232724518
<button id="getlist">獲取列表</button>
<button id="login">登錄</button>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
axios.defaults.withCredentials = true;
getlist.onclick = () => {
axios.get("/api/corslist").then(res => {
console.log(res.data);
});
};
login.onclick = () => {
axios.post("/api/login");
};
</script>
router.get("/api/corslist", async ctx => {
ctx.body = {
data: [{ name: "秋風的筆記" }]
};
});
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
};
});
訪問 http://localhost:8000/charles
image-20200412232231554
image-20200412232752837
完美解決。
唔。這里又有一個注意點。在 Mac mojave 10.14
中會出現 charles
抓不到本地包的情況。這個時候需要自定義一個域名,然后配置hosts
指定到127.0.0.1
。然后修改訪問方式 http://localhost.charlesproxy.com:8000/charles
。
image-20200412233258107
image-20200412233317027
Nginx 則是通過反向代理的方式,(這里也需要自定義一個域名)這里就是保證我當前域,能獲取到靜態資源和接口,不關心是怎么獲取的。nginx 安裝教程
image-20200412233536522
配置下 hosts
127.0.0.1 local.test
配置 nginx
server {
listen 80;
server_name local.test;
location /api {
proxy_pass http://localhost:8080;
}
location / {
proxy_pass http://localhost:8000;
}
}
啟動 nginx
sudo nginx
重啟 nginx
sudo nginx -s reload
前端代碼
<script>
axios.defaults.withCredentials = true;
getlist.onclick = () => {
axios.get("/api/corslist").then(res => {
console.log(res.data);
});
};
login.onclick = () => {
axios.post("/api/login");
};
</script>
后端代碼
router.get("/api/corslist", async ctx => {
ctx.body = {
data: [{ name: "秋風的筆記" }]
};
});
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
};
});
訪問 http://local.test/charles
瀏覽器顯示
image-20200413000229326
JSONP
主要就是利用了 script
標簽沒有跨域限制的這個特性來完成的。
「使用限制」
僅支持 GET 方法,如果想使用完整的 REST 接口,請使用 CORS 或者其他代理方式。
「流程解析」
1.前端定義解析函數(例如 jsonpCallback=function(){....})
2.通過 params 形式包裝請求參數,并且聲明執行函數(例如 cb=jsonpCallback)
3.后端獲取前端聲明的執行函數(jsonpCallback),并以帶上參數并調用執行函數的方式傳遞給前端。
「使用示例」
后端實現
const Koa = require("koa");
const fs = require("fs");
const app = new Koa();
app.use(async (ctx, next) => {
if (ctx.path === "/api/jsonp") {
const { cb, msg } = ctx.query;
ctx.body = `${cb}(${JSON.stringify({ msg })})`;
return;
}
});
app.listen(8080);
普通 js 示例
<script type="text/javascript">
window.jsonpCallback = function(res) {
console.log(res);
};
</script>
<script
src="http://localhost:8080/api/jsonp?msg=hello&cb=jsonpCallback"
type="text/javascript"
></script>
JQuery Ajax 示例
<script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script>
<script>
$.ajax({
url: "http://localhost:8080/api/jsonp",
dataType: "jsonp",
type: "get",
data: {
msg: "hello"
},
jsonp: "cb",
success: function(data) {
console.log(data);
}
});
</script>
「原理解析」
其實這就是 js 的魔法
我們先來看最簡單的 js 調用。嗯,很自然的調用。
<script>
window.jsonpCallback = function(res) {
console.log(res);
};
</script>
<script>
jsonpCallback({ a: 1 });
</script>
我們稍稍改造一下,外鏈的形式。
<script>
window.jsonpCallback = function(res) {
console.log(res);
};
</script>
<script src="http://localhost:8080/api/a.js"></script>
// http://localhost:8080/api/a.js jsonpCallback({a:1});
我們再改造一下,我們把這個外鏈的 js 就當做是一個動態的接口,因為本身資源和接口一樣,是一個請求,也包含各種參數,也可以動態化返回。
<script>
window.jsonpCallback = function(res) {
console.log(res);
};
</script>
<script src="http://localhost:8080/api/a.js?a=123&cb=sonpCallback"></script>
// http://localhost:8080/api/a.js jsonpCallback({a:123});
你仔細品,細細品,是不是 jsonp 有的優勢就是 script 加載 js 的優勢,加載的方式只不過換了一種說法。這也告訴我們一個道理,很多東西并沒有那么神奇,是在你所學的知識范圍內。就好比,桃樹和柳樹,如果你把他們當成很大跨度的東西去記憶理解,那么世上這么多樹,你真的要累死了,你把他們都當成是樹,哦吼?你會突然發現,你對世界上所有的樹都有所了解,他們都會長葉子,光合作用....當然也有個例,但是你只需要去記憶這些細微的差別,抓住主干。。。嗯,反正就這么個道理。
WebSocket 規范定義了一種 API,可在網絡瀏覽器和服務器之間建立“套接字”連接。簡單地說:客戶端和服務器之間存在持久的連接,而且雙方都可以隨時開始發送數據。詳細教程可以看 https://www.html5rocks.com/zh/tutorials/websockets/basics/
這種方式本質沒有使用了 HTTP, 因此也沒有跨域的限制,沒有什么過多的解釋直接上代碼吧。
前端部分
<script>
let socket = new WebSocket("ws://localhost:8080");
socket.onopen = function() {
socket.send("秋風的筆記");
};
socket.onmessage = function(e) {
console.log(e.data);
};
</script>
后端部分
const WebSocket = require("ws");
const server = new WebSocket.Server({ port: 8080 });
server.on("connection", function(socket) {
socket.on("message", function(data) {
socket.send(data);
});
});
「window.postMessage()」 方法可以安全地實現跨源通信。通常,對于兩個不同頁面的腳本,只有當執行它們的頁面位于具有相同的協議(通常為 https),端口號(443 為 https 的默認值),以及主機 (兩個頁面的模數 Document.domain
設置為相同的值) 時,這兩個腳本才能相互通信。「window.postMessage()」 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。
1.頁面和其打開的新窗口的數據傳遞
2.多窗口之間消息傳遞
3.頁面與嵌套的 iframe 消息傳遞
詳細用法看 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
otherWindow.postMessage(message, targetOrigin, [transfer]);
Transferable
對象. 這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權index.html
<iframe
src="http://localhost:8080"
frameborder="0"
id="iframe"
onload="load()"
></iframe>
<script>
function load() {
iframe.contentWindow.postMessage("秋風的筆記", "http://localhost:8080");
window.onmessage = e => {
console.log(e.data);
};
}
</script>
another.html
<div>hello</div>
<script>
window.onmessage = e => {
console.log(e.data); // 秋風的筆記
e.source.postMessage(e.data, e.origin);
};
</script>
從第 7 種到第 9 種方式,我覺得別人的寫的已經很好了,為了完整性,我就拿別人的了。如有雷同....(不對,就是雷同....)不要說不出來。
「該方式只能用于二級域名相同的情況下,比如 a.test.com
和 b.test.com
適用于該方式」。只需要給頁面添加 document.domain ='test.com'
表示二級域名都相同就可以實現跨域。
www. baidu. com .
三級域 二級域 頂級域 根域
// a.test.com
<body>
helloa
<iframe
src="http://b.test.com/b.html"
frameborder="0"
onload="load()"
id="frame"
></iframe>
<script>
document.domain = "test.com";
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.test.com
<body>
hellob
<script>
document.domain = "test.com";
var a = 100;
</script>
</body>
原理就是通過 url 帶 hash ,通過一個非跨域的中間頁面來傳遞數據。
一開始 a.html 給 c.html 傳一個 hash 值,然后 c.html 收到 hash 值后,再把 hash 值傳遞給 b.html,最后 b.html 將結果放到 a.html 的 hash 值中。同樣的,a.html 和 b.htm l 是同域的,都是 http://localhost:8000
,而 c.html 是http://localhost:8080
// a.html
<iframe src="http://localhost:8080/hash/c.html#name1"></iframe>
<script>
console.log(location.hash);
window.onhashchange = function() {
console.log(location.hash);
};
</script>
// b.html
<script>
window.parent.parent.location.hash = location.hash;
</script>
// c.html
<body></body>
<script>
console.log(location.hash);
const iframe = document.createElement("iframe");
iframe.src = "http://localhost:8000/hash/b.html#name2";
document.body.appendChild(iframe);
</script>
window 對象的 name 屬性是一個很特別的屬性,當該 window 的 location 變化,然后重新加載,它的 name 屬性可以依然保持不變。
其中 a.html 和 b.html 是同域的,都是http://localhost:8000
,而 c.html 是http://localhost:8080
// a.html
<iframe
src="http://localhost:8080/name/c.html"
frameborder="0"
onload="load()"
id="iframe"
></iframe>
<script>
let first = true;
// onload事件會觸發2次,第1次加載跨域頁,并留存數據于window.name
function load() {
if (first) {
// 第1次onload(跨域頁)成功后,切換到同域代理頁面
iframe.src = "http://localhost:8000/name/b.html";
first = false;
} else {
// 第2次onload(同域b.html頁)成功后,讀取同域window.name中數據
console.log(iframe.contentWindow.name);
}
}
</script>
b.html 為中間代理頁,與 a.html 同域,內容為空。
// b.html
<div></div>
// c.html
<script>
window.name = "秋風的筆記";
</script>
通過 iframe 的 src 屬性由外域轉向本地域,跨域數據即由 iframe 的 window.name 從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。
其實講下其實跨域問題是瀏覽器策略,源頭是他,那么能否能關閉這個功能呢?
答案是肯定的。
「注意事項: 因為瀏覽器是眾多 web 頁面入口。我們是否也可以像客戶端那種,就是用一個單獨的專門宿主瀏覽器,來打開調試我們的開發頁面。例如這里以 chrome canary 為例,這個是我專門調試頁面的瀏覽器,不會用它來訪問其他 web url。因此它也相對于安全一些。當然這個方式,只限于當你真的被跨域折磨地崩潰的時候才建議使用以下。使用后,請以正常的方式將他打開,以免你不小心用這個模式干了其他的事?!?/strong>
找到你安裝的目錄
.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx
~/Downloads/chrome-data
這個目錄可以自定義.
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --disable-web-security --user-data-dir=~/Downloads/chrome-data
image-20200413143102377
嗯,使用起來很香,但是再次提醒,一般情況千萬別輕易使用這個方式,這個方式好比七傷拳,使用的好威力無比,使用不好,很容易傷到自己。
在最一開始,我們知道了,跨域只存在于瀏覽器端。而瀏覽器為 web 提供訪問入口。我們在可以瀏覽器內打開很多頁面。正是這樣的開放形態,所以我們需要對他有所限制。就比如林子大了,什么鳥都有,我們需要有一個統一的規范來進行約定才能保障這個安全性。
這里還是用最常用的方式來講解,例如用戶登錄 a 網站,同時新開 tab 打開了 b 網站,如果不限制同源, b 可以像 a 網站發起任何請求,會讓不法分子有機可趁。
我舉個例子吧, 你先登錄下 www.baidu.com ,然后訪問我這個網址。
https://zerolty.com/node-demo/index.html
image-20200413190413758
你會發現,這個和真實的百度一模一樣,如果再把域名搞的相似一些,是不是很容易被騙,如果可以進行 dom 操作...那么大家的信息在這種釣魚網站眼里都是一顆顆小白菜,等著被收割。
?可以在 http 返回頭 添加
X-Frame-Options: SAMEORIGIN
防止被別人添加至 iframe。?
以上最常用的就是前 4 種方式,特別是第 2 種非常常見,我里面也提到了多種示例,大家可以慢慢消化一下。希望未來有更加安全的方式來限制 web ,解決跨域的頭疼,哈哈哈哈。
「有一個不成熟的想法,可以搞這么一個瀏覽器,只讓訪問內網/本地網絡,專門給開發者用來調試頁面用,對于靜態資源可以配置白名單,這樣是不是就沒有跨域問題了,23333。上述如有錯誤,請第一時間指出,我會進行修改,以免給大家來誤導?!?/strong>
https://stackoverflow.com/questions/12296910/so-jsonp-or-cors
https://juejin.im/post/5c23993de51d457b8c1f4ee1#heading-18
https://juejin.im/post/5a6320d56fb9a01cb64ee191
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
最多閱讀