10 種跨域解決方案(附終極方案)
寫(xiě)在前面
嗯。又來(lái)了,又說(shuō)到跨域了,這是一個(gè)老生常談的話題,以前我覺(jué)得這種基礎(chǔ)文章沒(méi)有什么好寫(xiě)的,會(huì)想著你去了解底層啊,不是很簡(jiǎn)單嗎。但是最近在開(kāi)發(fā)一個(gè) 「vscode 插件」 發(fā)現(xiàn),當(dāng)你剛?cè)腴T(mén)一樣?xùn)|西的時(shí)候,你不會(huì)想這么多,因?yàn)槟銓?duì)他不熟悉,當(dāng)你遇到不會(huì)的東西,你就是想先找到解決方案,然后通過(guò)這個(gè)解決方案再去深入理解。就比如跨域,新人或者剛接觸的人對(duì)它并不是那么熟悉,所以說(shuō)列出一些自己積累的方案,以及一些常用的場(chǎng)景來(lái)給他人帶來(lái)一些解決問(wèn)題的思路,這件事是有意義的。(寫(xiě)完之后還發(fā)現(xiàn)真香。以后忘了還能回來(lái)看看)
其實(shí)現(xiàn)在的環(huán)境對(duì)于剛?cè)腴T(mén)的前端來(lái)說(shuō),非常的不友好,一方面吧,很多剛新人沒(méi)有經(jīng)歷過(guò)工具的變更時(shí)代,另一方面框架的迭代更新速度很快。在以前你可能掌握幾種常見(jiàn)的手法就好了。但是現(xiàn)在 webpack-dev-server、vue-cli、parcel,這些腳手架都進(jìn)行了一層封裝,對(duì)于熟悉的人可能很簡(jiǎn)單,但是對(duì)于還未入門(mén)的人來(lái)說(shuō),簡(jiǎn)直就是一個(gè)黑盒,雖然用起來(lái)很方便,但是某一天遇到了問(wèn)題,你對(duì)它不熟悉,你就會(huì)不知道所錯(cuò)。(但是別慌,主流 cli 的跨域方式我都會(huì)講到)
講著講著有點(diǎn)偏離方向,可能我講的也并不一定是正確的。下面切入正題。
本文會(huì)以 「「What-How-Why」」 的方式來(lái)進(jìn)行講解。而在在 How (如何解決跨域,將會(huì)提供標(biāo)題的 11 種方案。)
「重要的說(shuō)明: 在文中,web 端地址為 localhost:8000 服務(wù)端地址為 localhost:8080,這一點(diǎn)希望你能記住,會(huì)貫穿全文,你也可以把此處的兩端的地址代入你自己的地址。」

以下所有代碼均在 https://github.com/hua1995116/node-demo/tree/master/node-cors

一、跨域是什么?
1.同源策略
跨域問(wèn)題其實(shí)就是瀏覽器的同源策略所導(dǎo)致的。
?「同源策略」是一個(gè)重要的安全策略,它用于限制一個(gè)origin的文檔或者它加載的腳本如何能與另一個(gè)源的資源進(jìn)行交互。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介。
--來(lái)源 MDN
?
當(dāng)跨域時(shí)會(huì)收到以下錯(cuò)誤

2.同源示例
那么如何才算是同源呢?先來(lái)看看 url 的組成部分
http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument

只有當(dāng)
「protocol(協(xié)議)、domain(域名)、port(端口)三者一致。」
「protocol(協(xié)議)、domain(域名)、port(端口)三者一致。」
「protocol(協(xié)議)、domain(域名)、port(端口)三者一致。」
才是同源。
以下協(xié)議、域名、端口一致。
http://www.example.com:80/a.js
http://www.example.com:80/b.js
以下這種看上去再相似也沒(méi)有用,都不是同源。
http://www.example.com:8080
http://www2.example.com:80
在這里注意一下啊,這里是為了突出端口的區(qū)別才寫(xiě)上端口。在默認(rèn)情況下 http 可以省略端口 80, https 省略 443。這別到時(shí)候鬧笑話了,你和我說(shuō) http://www.example.com:80 和 http://www.example.com 不是同源,他倆是一個(gè)東西。
http://www.example.com:80 === http://www.example.com
https://www.example.com:443 === https://www.example.com
唔,還是要說(shuō)明一下。
二、如何解決跨域?
1.CORS
跨域資源共享(CORS) 是一種機(jī)制,它使用額外的 HTTP 頭來(lái)告訴瀏覽器 讓運(yùn)行在一個(gè) origin (domain) 上的 Web 應(yīng)用被準(zhǔn)許訪問(wèn)來(lái)自不同源服務(wù)器上的指定的資源。當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器「不同的域、協(xié)議或端口」請(qǐng)求一個(gè)資源時(shí),資源會(huì)發(fā)起一個(gè)「跨域 HTTP 請(qǐng)求」。
而在 cors 中會(huì)有 簡(jiǎn)單請(qǐng)求 和 復(fù)雜請(qǐng)求的概念。
「瀏覽器支持情況」
當(dāng)你使用 IE<=9, Opera<12, or Firefox<3.5 或者更加老的瀏覽器,這個(gè)時(shí)候請(qǐng)使用 JSONP 。
a.簡(jiǎn)單請(qǐng)求
不會(huì)觸發(fā) CORS 預(yù)檢請(qǐng)求。這樣的請(qǐng)求為“簡(jiǎn)單請(qǐng)求”,請(qǐng)注意,該術(shù)語(yǔ)并不屬于 Fetch (其中定義了 CORS)規(guī)范。若請(qǐng)求滿足所有下述條件,則該請(qǐng)求可視為“簡(jiǎn)單請(qǐng)求”:
情況一: 使用以下方法(意思就是以下請(qǐng)求意外的都是非簡(jiǎn)單請(qǐng)求)
GETHEADPOST
情況二: 人為設(shè)置以下集合外的請(qǐng)求頭
AcceptAccept-LanguageContent-LanguageContent-Type(需要注意額外的限制)DPRDownlinkSave-DataViewport-WidthWidth
情況三:Content-Type的值僅限于下列三者之一:(例如 application/json 為非簡(jiǎn)單請(qǐng)求)
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
情況四:
請(qǐng)求中的任意XMLHttpRequestUpload 對(duì)象均沒(méi)有注冊(cè)任何事件監(jiān)聽(tīng)器;XMLHttpRequestUpload 對(duì)象可以使用 XMLHttpRequest.upload 屬性訪問(wèn)。
情況五:
請(qǐng)求中沒(méi)有使用 ReadableStream 對(duì)象。
b.非簡(jiǎn)單請(qǐng)求
除以上情況外的。
c.Node 中的解決方案
原生方式
我們來(lái)看下后端部分的解決方案。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());
關(guān)于 cors 的 cookie 問(wèn)題
想要傳遞 cookie 需要滿足 3 個(gè)條件
1.web 請(qǐng)求設(shè)置withCredentials
這里默認(rèn)情況下在跨域請(qǐng)求,瀏覽器是不帶 cookie 的。但是我們可以通過(guò)設(shè)置 withCredentials 來(lái)進(jìn)行傳遞 cookie.
//?原生?xml?的設(shè)置方式
var?xhr?=?new?XMLHttpRequest();
xhr.withCredentials?=?true;
//?axios?設(shè)置方式
axios.defaults.withCredentials?=?true;
2.Access-Control-Allow-Credentials 為 true
3.Access-Control-Allow-Origin為非 *
這里請(qǐng)求的方式,在 chrome 中是能看到返回值的,但是只要不滿足以上其一,瀏覽器會(huì)報(bào)錯(cuò),獲取不到返回值。

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.

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.

d.前端示例
分別演示一下前端部分 簡(jiǎn)單請(qǐng)求 和 非簡(jiǎn)單請(qǐng)求
簡(jiǎn)單請(qǐng)求
<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>
非簡(jiǎn)單請(qǐng)求
這里我們加入了一個(gè)非集合內(nèi)的 header 頭 cc 來(lái)達(dá)到非簡(jiǎn)單請(qǐng)求的目的。
<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>


小結(jié)
1、 在新版的 chrome 中,如果你發(fā)送了復(fù)雜請(qǐng)求,你卻看不到 options 請(qǐng)求。可以在這里設(shè)置 chrome://flags/#out-of-blink-cors 設(shè)置成 disbale ,重啟瀏覽器。對(duì)于非簡(jiǎn)單請(qǐng)求就能看到 options 請(qǐng)求了。
2、 一般情況下后端接口是不會(huì)開(kāi)啟這個(gè)跨域頭的,除非是一些與用戶無(wú)關(guān)的不太重要的接口。
2.Node 正向代理
代理的思路為,利用服務(wù)端請(qǐng)求不會(huì)跨域的特性,讓接口和當(dāng)前站點(diǎn)同域。
「代理前」

「代理后」

這樣,所有的資源以及請(qǐng)求都在一個(gè)域名下了。
a.cli 工具中的代理
1) Webpack (4.x)
在webpack中可以配置proxy來(lái)快速獲得接口代理的能力。
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"
????})
??]
};
修改前端接口請(qǐng)求方式,改為不帶域名。(因?yàn)楝F(xiàn)在是同域請(qǐng)求了)
<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>
2) Vue-cli 2.x
//?config/index.js
...
proxyTable:?{
??'/api':?{
?????target:?'http://localhost:8080',
??}
},
...
3) Vue-cli 3.x
// vue.config.js 如果沒(méi)有就新建
module.exports?=?{
??devServer:?{
????port:?8000,
????proxy:?{
??????"/api":?{
????????target:?"http://localhost:8080"
??????}
????}
??}
};
4) Parcel (2.x)
// .proxyrc
{
"/api": {
"target": "http://localhost:8080"
}
}
看到這里,心里一句 xxx 就會(huì)脫口而出,一會(huì)寫(xiě)配置文件,一會(huì) proxyTable ,一會(huì) proxy,怎么這么多的形式?學(xué)不動(dòng)了學(xué)不動(dòng)了。。。不過(guò)也不用慌,還是有方法的。以上所有配置都是有著共同的底層包 http-proxy-middleware .里面需要用到的各種 websocket ,rewrite 等功能,直接看這個(gè)庫(kù)的配置就可以了。關(guān)于 http-proxy-middleware 這個(gè)庫(kù)的原理可以看我這篇文章 https://github.com/hua1995116/proxy 當(dāng)然了。。。對(duì)于配置的位置入口還是非統(tǒng)一的。教一個(gè)搜索的技巧吧,上面配置寫(xiě)哪里都不用記的,想要哪個(gè)框架的 直接 google 搜索 xxx proxy 就行了。
例如 vue-cli 2 proxy 、 webpack proxy 等等....基本會(huì)搜到有官網(wǎng)的配置答案,通用且 nice。
b.使用自己的代理工具
cors-anywhere
「服務(wù)端」
//?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>
「效果展示」

「缺點(diǎn)」
無(wú)法專遞 cookie,原因是因?yàn)檫@個(gè)是一個(gè)代理庫(kù),作者覺(jué)得中間傳遞 cookie 太不安全了。https://github.com/Rob--W/cors-anywhere/issues/208#issuecomment-575254153
c.charles
介紹
這是一個(gè)測(cè)試、開(kāi)發(fā)的神器。介紹與使用
利用 charles 進(jìn)行跨域,本質(zhì)就是請(qǐng)求的攔截與代理。
在 tools/map remote 中設(shè)置代理


前端代碼
<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:?"秋風(fēng)的筆記"?}]
??};
});
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
??};
});
效果
訪問(wèn) http://localhost:8000/charles


完美解決。
唔。這里又有一個(gè)注意點(diǎn)。在 Mac mojave 10.14 中會(huì)出現(xiàn) charles 抓不到本地包的情況。這個(gè)時(shí)候需要自定義一個(gè)域名,然后配置hosts指定到127.0.0.1。然后修改訪問(wèn)方式 http://localhost.charlesproxy.com:8000/charles。


3.Nginx 反向代理
介紹
Nginx 則是通過(guò)反向代理的方式,(這里也需要自定義一個(gè)域名)這里就是保證我當(dāng)前域,能獲取到靜態(tài)資源和接口,不關(guān)心是怎么獲取的。nginx 安裝教程

配置下 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;
}
}
啟動(dòng) nginx
sudo nginx
重啟 nginx
sudo nginx -s reload
實(shí)現(xiàn)
前端代碼
<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:?"秋風(fēng)的筆記"?}]
??};
});
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
??};
});
效果
訪問(wèn) http://local.test/charles
瀏覽器顯示

4.JSONP
JSONP 主要就是利用了 script 標(biāo)簽沒(méi)有跨域限制的這個(gè)特性來(lái)完成的。
「使用限制」
僅支持 GET 方法,如果想使用完整的 REST 接口,請(qǐng)使用 CORS 或者其他代理方式。
「流程解析」
1.前端定義解析函數(shù)(例如 jsonpCallback=function(){....})
2.通過(guò) params 形式包裝請(qǐng)求參數(shù),并且聲明執(zhí)行函數(shù)(例如 cb=jsonpCallback)
3.后端獲取前端聲明的執(zhí)行函數(shù)(jsonpCallback),并以帶上參數(shù)并調(diào)用執(zhí)行函數(shù)的方式傳遞給前端。
「使用示例」
后端實(shí)現(xiàn)
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>
「原理解析」
其實(shí)這就是 js 的魔法
我們先來(lái)看最簡(jiǎn)單的 js 調(diào)用。嗯,很自然的調(diào)用。
<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});
我們?cè)俑脑煲幌拢覀儼堰@個(gè)外鏈的 js 就當(dāng)做是一個(gè)動(dòng)態(tài)的接口,因?yàn)楸旧碣Y源和接口一樣,是一個(gè)請(qǐng)求,也包含各種參數(shù),也可以動(dòng)態(tài)化返回。
<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});
你仔細(xì)品,細(xì)細(xì)品,是不是 jsonp 有的優(yōu)勢(shì)就是 script 加載 js 的優(yōu)勢(shì),加載的方式只不過(guò)換了一種說(shuō)法。這也告訴我們一個(gè)道理,很多東西并沒(méi)有那么神奇,是在你所學(xué)的知識(shí)范圍內(nèi)。就好比,桃樹(shù)和柳樹(shù),如果你把他們當(dāng)成很大跨度的東西去記憶理解,那么世上這么多樹(shù),你真的要累死了,你把他們都當(dāng)成是樹(shù),哦吼?你會(huì)突然發(fā)現(xiàn),你對(duì)世界上所有的樹(shù)都有所了解,他們都會(huì)長(zhǎng)葉子,光合作用....當(dāng)然也有個(gè)例,但是你只需要去記憶這些細(xì)微的差別,抓住主干。。。嗯,反正就這么個(gè)道理。
5.Websocket
WebSocket 規(guī)范定義了一種 API,可在網(wǎng)絡(luò)瀏覽器和服務(wù)器之間建立“套接字”連接。簡(jiǎn)單地說(shuō):客戶端和服務(wù)器之間存在持久的連接,而且雙方都可以隨時(shí)開(kāi)始發(fā)送數(shù)據(jù)。詳細(xì)教程可以看 https://www.html5rocks.com/zh/tutorials/websockets/basics/
這種方式本質(zhì)沒(méi)有使用了 HTTP, 因此也沒(méi)有跨域的限制,沒(méi)有什么過(guò)多的解釋直接上代碼吧。
前端部分
<script>
??let?socket?=?new?WebSocket("ws://localhost:8080");
??socket.onopen?=?function()?{
????socket.send("秋風(fēng)的筆記");
??};
??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);
??});
});
6.window.postMessage
「window.postMessage()」 方法可以安全地實(shí)現(xiàn)跨源通信。通常,對(duì)于兩個(gè)不同頁(yè)面的腳本,只有當(dāng)執(zhí)行它們的頁(yè)面位于具有相同的協(xié)議(通常為 https),端口號(hào)(443 為 https 的默認(rèn)值),以及主機(jī) (兩個(gè)頁(yè)面的模數(shù) Document.domain設(shè)置為相同的值) 時(shí),這兩個(gè)腳本才能相互通信。「window.postMessage()」 方法提供了一種受控機(jī)制來(lái)規(guī)避此限制,只要正確的使用,這種方法就很安全。
用途
1.頁(yè)面和其打開(kāi)的新窗口的數(shù)據(jù)傳遞
2.多窗口之間消息傳遞
3.頁(yè)面與嵌套的 iframe 消息傳遞
用法
詳細(xì)用法看 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow: 其他窗口的一個(gè)引用,比如 iframe 的 contentWindow 屬性、執(zhí)行window.open返回的窗口對(duì)象、或者是命名過(guò)或數(shù)值索引的window.frames。
message: 將要發(fā)送到其他 window 的數(shù)據(jù)。
targetOrigin: 通過(guò)窗口的 origin 屬性來(lái)指定哪些窗口能接收到消息事件.
transfer(可選) : 是一串和 message 同時(shí)傳遞的
Transferable對(duì)象. 這些對(duì)象的所有權(quán)將被轉(zhuǎn)移給消息的接收方,而發(fā)送一方將不再保有所有權(quán)
示例
index.html
<iframe
??src="http://localhost:8080"
??frameborder="0"
??id="iframe"
??onload="load()"
>iframe>
<script>
??function?load()?{
????iframe.contentWindow.postMessage("秋風(fēng)的筆記",?"http://localhost:8080");
????window.onmessage?=?e?=>?{
??????console.log(e.data);
????};
??}
script>
another.html
<div>hellodiv>
<script>
??window.onmessage?=?e?=>?{
????console.log(e.data);?//?秋風(fēng)的筆記
????e.source.postMessage(e.data,?e.origin);
??};
script>
7.document.domain + Iframe
從第 7 種到第 9 種方式,我覺(jué)得別人的寫(xiě)的已經(jīng)很好了,為了完整性,我就拿別人的了。如有雷同....(不對(duì),就是雷同....)不要說(shuō)不出來(lái)。
「該方式只能用于二級(jí)域名相同的情況下,比如 a.test.com 和 b.test.com 適用于該方式」。只需要給頁(yè)面添加 document.domain ='test.com' 表示二級(jí)域名都相同就可以實(shí)現(xiàn)跨域。
www.???baidu.??com?????.
三級(jí)域??二級(jí)域???頂級(jí)域???根域
//?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>
8.window.location.hash + Iframe
實(shí)現(xiàn)原理
原理就是通過(guò) url 帶 hash ,通過(guò)一個(gè)非跨域的中間頁(yè)面來(lái)傳遞數(shù)據(jù)。
實(shí)現(xiàn)流程
一開(kāi)始 a.html 給 c.html 傳一個(gè) hash 值,然后 c.html 收到 hash 值后,再把 hash 值傳遞給 b.html,最后 b.html 將結(jié)果放到 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>
9.window.name + Iframe
window 對(duì)象的 name 屬性是一個(gè)很特別的屬性,當(dāng)該 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事件會(huì)觸發(fā)2次,第1次加載跨域頁(yè),并留存數(shù)據(jù)于window.name
??function?load()?{
????if?(first)?{
??????//?第1次onload(跨域頁(yè))成功后,切換到同域代理頁(yè)面
??????iframe.src?=?"http://localhost:8000/name/b.html";
??????first?=?false;
????}?else?{
??????//?第2次onload(同域b.html頁(yè))成功后,讀取同域window.name中數(shù)據(jù)
??????console.log(iframe.contentWindow.name);
????}
??}
script>
b.html 為中間代理頁(yè),與 a.html 同域,內(nèi)容為空。
//?b.html
<div>div>
//?c.html
<script>
??window.name?=?"秋風(fēng)的筆記";
script>
通過(guò) iframe 的 src 屬性由外域轉(zhuǎn)向本地域,跨域數(shù)據(jù)即由 iframe 的 window.name 從外域傳遞到本地域。這個(gè)就巧妙地繞過(guò)了瀏覽器的跨域訪問(wèn)限制,但同時(shí)它又是安全操作。
10.瀏覽器開(kāi)啟跨域(終極方案)
其實(shí)講下其實(shí)跨域問(wèn)題是瀏覽器策略,源頭是他,那么能否能關(guān)閉這個(gè)功能呢?
答案是肯定的。
「注意事項(xiàng): 因?yàn)闉g覽器是眾多 web 頁(yè)面入口。我們是否也可以像客戶端那種,就是用一個(gè)單獨(dú)的專門(mén)宿主瀏覽器,來(lái)打開(kāi)調(diào)試我們的開(kāi)發(fā)頁(yè)面。例如這里以 chrome canary 為例,這個(gè)是我專門(mén)調(diào)試頁(yè)面的瀏覽器,不會(huì)用它來(lái)訪問(wèn)其他 web url。因此它也相對(duì)于安全一些。當(dāng)然這個(gè)方式,只限于當(dāng)你真的被跨域折磨地崩潰的時(shí)候才建議使用以下。使用后,請(qǐng)以正常的方式將他打開(kāi),以免你不小心用這個(gè)模式干了其他的事。」
Windows
找到你安裝的目錄
.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx
Mac
~/Downloads/chrome-data 這個(gè)目錄可以自定義.
/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --disable-web-security --user-data-dir=~/Downloads/chrome-data
效果展示

嗯,使用起來(lái)很香,但是再次提醒,一般情況千萬(wàn)別輕易使用這個(gè)方式,這個(gè)方式好比七傷拳,使用的好威力無(wú)比,使用不好,很容易傷到自己。
三、為什么需要跨域?
在最一開(kāi)始,我們知道了,跨域只存在于瀏覽器端。而瀏覽器為 web 提供訪問(wèn)入口。我們?cè)诳梢詾g覽器內(nèi)打開(kāi)很多頁(yè)面。正是這樣的開(kāi)放形態(tài),所以我們需要對(duì)他有所限制。就比如林子大了,什么鳥(niǎo)都有,我們需要有一個(gè)統(tǒng)一的規(guī)范來(lái)進(jìn)行約定才能保障這個(gè)安全性。
1.限制不同源的請(qǐng)求
這里還是用最常用的方式來(lái)講解,例如用戶登錄 a 網(wǎng)站,同時(shí)新開(kāi) tab 打開(kāi)了 b 網(wǎng)站,如果不限制同源, b 可以像 a 網(wǎng)站發(fā)起任何請(qǐng)求,會(huì)讓不法分子有機(jī)可趁。
2.限制 dom 操作
我舉個(gè)例子吧, 你先登錄下 www.baidu.com ,然后訪問(wèn)我這個(gè)網(wǎng)址。
https://zerolty.com/node-demo/index.html

你會(huì)發(fā)現(xiàn),這個(gè)和真實(shí)的百度一模一樣,如果再把域名搞的相似一些,是不是很容易被騙,如果可以進(jìn)行 dom 操作...那么大家的信息在這種釣魚(yú)網(wǎng)站眼里都是一顆顆小白菜,等著被收割。
?可以在 http 返回頭 添加
?X-Frame-Options: SAMEORIGIN防止被別人添加至 iframe。
寫(xiě)在最后
以上最常用的就是前 4 種方式,特別是第 2 種非常常見(jiàn),我里面也提到了多種示例,大家可以慢慢消化一下。希望未來(lái)有更加安全的方式來(lái)限制 web ,解決跨域的頭疼,哈哈哈哈。
「有一個(gè)不成熟的想法,可以搞這么一個(gè)瀏覽器,只讓訪問(wèn)內(nèi)網(wǎng)/本地網(wǎng)絡(luò),專門(mén)給開(kāi)發(fā)者用來(lái)調(diào)試頁(yè)面用,對(duì)于靜態(tài)資源可以配置白名單,這樣是不是就沒(méi)有跨域問(wèn)題了,23333。上述如有錯(cuò)誤,請(qǐng)第一時(shí)間指出,我會(huì)進(jìn)行修改,以免給大家來(lái)誤導(dǎo)。」
歡迎關(guān)注公眾號(hào) 「「秋風(fēng)的筆記」」,主要記錄日常中覺(jué)得有意思的工具以及分享開(kāi)發(fā)實(shí)踐,保持深度和專注度。
參考
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
小獅子有話說(shuō)
你好,我是 Chocolate,一個(gè)獅子座的前端攻城獅,希望成為優(yōu)秀的前端博主,每周都會(huì)更新文章,與你一起變優(yōu)秀~
關(guān)注 小獅子前端,回復(fù)【小獅子】獲取為大家整理好的文章、資源合集我的博客地址: yangchaoyi.vip歡迎收藏,可在博客留言板留下你的足跡,一起交流~覺(jué)得文章不錯(cuò),【 點(diǎn)贊】【在看】支持一波 ??ヽ(°▽°)ノ?
叮咚~ 可以給小獅子加
星標(biāo),便于查找。感謝加入小獅子前端,最好的我們最美的遇見(jiàn),我們下期再見(jiàn)~
