關(guān)于跨域的10種解決方案

一篇關(guān)于跨域文章

寫在前面

嗯。又來了渠羞,又說到跨域了,這是一個老生常談的話題智哀,以前我覺得這種基礎(chǔ)文章沒有什么好寫的次询,會想著你去了解底層啊,不是很簡單嗎瓷叫。但是最近在開發(fā)一個 「vscode 插件」 發(fā)現(xiàn)屯吊,當(dāng)你剛?cè)腴T一樣?xùn)|西的時候,你不會想這么多摹菠,因為你對他不熟悉盒卸,當(dāng)你遇到不會的東西,你就是想先找到解決方案次氨,然后通過這個解決方案再去深入理解蔽介。就比如跨域,新人或者剛接觸的人對它并不是那么熟悉,所以說列出一些自己積累的方案虹蓄,以及一些常用的場景來給他人帶來一些解決問題的思路犀呼,這件事是有意義的。(寫完之后還發(fā)現(xiàn)真香薇组。以后忘了還能回來看看)

其實現(xiàn)在的環(huán)境對于剛?cè)腴T的前端來說外臂,非常的不友好,一方面吧律胀,很多剛新人沒有經(jīng)歷過工具的變更時代宋光,另一方面框架的迭代更新速度很快。在以前你可能掌握幾種常見的手法就好了炭菌。但是現(xiàn)在 webpack-dev-server球涛、vue-cli纪吮、parcel,這些腳手架都進行了一層封裝,對于熟悉的人可能很簡單堕仔,但是對于還未入門的人來說虐杯,簡直就是一個黑盒拱她,雖然用起來很方便篱昔,但是某一天遇到了問題,你對它不熟悉玛荞,你就會不知道所錯娇掏。(但是別慌,主流 cli 的跨域方式我都會講到)

講著講著有點偏離方向勋眯,可能我講的也并不一定是正確的婴梧。下面切入正題。

本文會以 「「What-How-Why」」 的方式來進行講解客蹋。而在在 How (如何解決跨域塞蹭,將會提供標(biāo)題的 11 種方案。)

「重要的說明: 在文中讶坯,web 端地址為 localhost:8000 服務(wù)端地址為 localhost:8080,這一點希望你能記住番电,會貫穿全文,你也可以把此處的兩端的地址代入你自己的地址辆琅∈欤」

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">cors</figcaption>

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

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200413192431636</figcaption>

一、跨域是什么婉烟?

1.同源策略

跨域問題其實就是瀏覽器的同源策略所導(dǎo)致的娩井。

?

「同源策略」是一個重要的安全策略,它用于限制一個origin的文檔或者它加載的腳本如何能與另一個源的資源進行交互似袁。它能幫助阻隔惡意文檔洞辣,減少可能被攻擊的媒介咐刨。

--來源 MDN

?

當(dāng)跨域時會收到以下錯誤

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200413205559124</figcaption>

2.同源示例

那么如何才算是同源呢?先來看看 url 的組成部分

http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412171942421</figcaption>

只有當(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

以下這種看上去再相似也沒有用,都不是同源沙郭。

http://www.example.com:8080

http://www2.example.com:80

在這里注意一下啊佛呻,這里是為了突出端口的區(qū)別才寫上端口。在默認(rèn)情況下 http 可以省略端口 80病线, https 省略 443吓著。這別到時候鬧笑話了,你和我說 http://www.example.com:80http://www.example.com 不是同源送挑,他倆是一個東西绑莺。

http://www.example.com:80 === http://www.example.com

https://www.example.com:443 === https://www.example.com

唔,還是要說明一下惕耕。

二纺裁、如何解決跨域?

1.CORS

跨域資源共享(CORS) 是一種機制赡突,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的 Web 應(yīng)用被準(zhǔn)許訪問來自不同源服務(wù)器上的指定的資源对扶。當(dāng)一個資源從與該資源本身所在的服務(wù)器「不同的域、協(xié)議或端口」請求一個資源時惭缰,資源會發(fā)起一個「跨域 HTTP 請求」浪南。

而在 cors 中會有 簡單請求復(fù)雜請求的概念。

「瀏覽器支持情況」

當(dāng)你使用 IE<=9, Opera<12, or Firefox<3.5 或者更加老的瀏覽器漱受,這個時候請使用 JSONP 络凿。

a.簡單請求

不會觸發(fā) CORS 預(yù)檢請求骡送。這樣的請求為“簡單請求”,請注意絮记,該術(shù)語并不屬于 Fetch (其中定義了 CORS)規(guī)范摔踱。若請求滿足所有下述條件,則該請求可視為“簡單請求”:

情況一: 使用以下方法(意思就是以下請求意外的都是非簡單請求)

  • GET
  • HEAD
  • POST

情況二: 人為設(shè)置以下集合外的請求頭

  • 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 對象均沒有注冊任何事件監(jiān)聽器怨愤;XMLHttpRequestUpload 對象可以使用 XMLHttpRequest.upload 屬性訪問派敷。

情況五:

請求中沒有使用 ReadableStream 對象。

b.非簡單請求

除以上情況外的撰洗。

c.Node 中的解決方案

原生方式

我們來看下后端部分的解決方案篮愉。NodeCORS 的解決代碼.

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 問題

想要傳遞 cookie 需要滿足 3 個條件

1.web 請求設(shè)置withCredentials

這里默認(rèn)情況下在跨域請求,瀏覽器是不帶 cookie 的差导。但是我們可以通過設(shè)置 withCredentials 來進行傳遞 cookie.

// 原生 xml 的設(shè)置方式var xhr = new XMLHttpRequest();xhr.withCredentials = true;// axios 設(shè)置方式axios.defaults.withCredentials = true;

2.Access-Control-Allow-Credentialstrue

3.Access-Control-Allow-Origin為非 *

這里請求的方式试躏,在 chrome 中是能看到返回值的,但是只要不滿足以上其一设褐,瀏覽器會報錯颠蕴,獲取不到返回值。

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412201424024</figcaption>

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

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412201411481</figcaption>

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

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412201530087</figcaption>

d.前端示例

分別演示一下前端部分 簡單請求非簡單請求

簡單請求
<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>
非簡單請求

這里我們加入了一個非集合內(nèi)的 headercc 來達到非簡單請求的目的助析。

<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

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412201158778</figcaption>

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412195829232</figcaption>

小結(jié)

1犀被、 在新版的 chrome 中,如果你發(fā)送了復(fù)雜請求貌笨,你卻看不到 options 請求弱判。可以在這里設(shè)置 chrome://flags/#out-of-blink-cors 設(shè)置成 disbale 锥惋,重啟瀏覽器昌腰。對于非簡單請求就能看到 options 請求了。

2膀跌、 一般情況下后端接口是不會開啟這個跨域頭的遭商,除非是一些與用戶無關(guān)的不太重要的接口。

2.Node 正向代理

代理的思路為捅伤,利用服務(wù)端請求不會跨域的特性劫流,讓接口和當(dāng)前站點同域。

「代理前」

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412202320482</figcaption>

「代理后」

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412202358759</figcaption>

這樣丛忆,所有的資源以及請求都在一個域名下了祠汇。

a.cli 工具中的代理

1) Webpack (4.x)

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"    })  ]};

修改前端接口請求方式熄诡,改為不帶域名可很。(因為現(xiàn)在是同域請求了)

<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 如果沒有就新建
module.exports = {  devServer: {    port: 8000,    proxy: {      "/api": {        target: "http://localhost:8080"      }    }  }};
4) Parcel (2.x)
// .proxyrc{  "/api": {    "target": "http://localhost:8080"  }}

看到這里,心里一句 xxx 就會脫口而出凰浮,一會寫配置文件我抠,一會 proxyTable 苇本,一會 proxy,怎么這么多的形式菜拓?學(xué)不動了學(xué)不動了瓣窄。。纳鼎。不過也不用慌俺夕,還是有方法的。以上所有配置都是有著共同的底層包 http-proxy-middleware .里面需要用到的各種 websocket 贱鄙,rewrite 等功能啥么,直接看這個庫的配置就可以了。關(guān)于 http-proxy-middleware 這個庫的原理可以看我這篇文章 https://github.com/hua1995116/proxy 當(dāng)然了贰逾。。菠秒。對于配置的位置入口還是非統(tǒng)一的疙剑。教一個搜索的技巧吧,上面配置寫哪里都不用記的践叠,想要哪個框架的 直接 google 搜索 xxx proxy 就行了言缤。

例如 vue-cli 2 proxy 、 webpack proxy 等等....基本會搜到有官網(wǎng)的配置答案禁灼,通用且 nice管挟。

b.使用自己的代理工具

cors-anywhere

「服務(wù)端」

// Listen on a specific host via the HOST environment variablevar host = process.env.HOST || "0.0.0.0";// Listen on a specific port via the PORT environment variablevar 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

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200413161243734</figcaption>

「缺點」

無法專遞 cookie,原因是因為這個是一個代理庫弄捕,作者覺得中間傳遞 cookie 太不安全了僻孝。https://github.com/Rob--W/cors-anywhere/issues/208#issuecomment-575254153

c.charles

介紹

這是一個測試、開發(fā)的神器守谓。介紹與使用

利用 charles 進行跨域穿铆,本質(zhì)就是請求的攔截與代理。

tools/map remote 中設(shè)置代理

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412232733437</figcaption>

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412232724518</figcaption>

前端代碼
<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  };});
效果

訪問 http://localhost:8000/charles

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412232231554</figcaption>

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412232752837</figcaption>

完美解決斋荞。

唔荞雏。這里又有一個注意點。在 Mac mojave 10.14 中會出現(xiàn) charles 抓不到本地包的情況平酿。這個時候需要自定義一個域名凤优,然后配置hosts指定到127.0.0.1。然后修改訪問方式 http://localhost.charlesproxy.com:8000/charles蜈彼。

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412233258107</figcaption>

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412233317027</figcaption>

3.Nginx 反向代理

介紹

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

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200412233536522</figcaption>

配置下 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

實現(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  };});
效果

訪問 http://local.test/charles

瀏覽器顯示

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200413000229326</figcaption>

4.JSONP

JSONP 主要就是利用了 script 標(biāo)簽沒有跨域限制的這個特性來完成的痒钝。

「使用限制」

僅支持 GET 方法,如果想使用完整的 REST 接口痢毒,請使用 CORS 或者其他代理方式送矩。

「流程解析」

1.前端定義解析函數(shù)(例如 jsonpCallback=function(){....})

2.通過 params 形式包裝請求參數(shù),并且聲明執(zhí)行函數(shù)(例如 cb=jsonpCallback)

3.后端獲取前端聲明的執(zhí)行函數(shù)(jsonpCallback)哪替,并以帶上參數(shù)并調(diào)用執(zhí)行函數(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>

「原理解析」

其實這就是 js 的魔法

我們先來看最簡單的 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});

我們再改造一下匆背,我們把這個外鏈的 js 就當(dāng)做是一個動態(tài)的接口,因為本身資源和接口一樣身冀,是一個請求钝尸,也包含各種參數(shù),也可以動態(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)勢就是 script 加載 js 的優(yōu)勢剩愧,加載的方式只不過換了一種說法猪叙。這也告訴我們一個道理,很多東西并沒有那么神奇仁卷,是在你所學(xué)的知識范圍內(nèi)穴翩。就好比,桃樹和柳樹五督,如果你把他們當(dāng)成很大跨度的東西去記憶理解藏否,那么世上這么多樹,你真的要累死了充包,你把他們都當(dāng)成是樹副签,哦吼?你會突然發(fā)現(xiàn)基矮,你對世界上所有的樹都有所了解淆储,他們都會長葉子,光合作用....當(dāng)然也有個例家浇,但是你只需要去記憶這些細(xì)微的差別本砰,抓住主干。钢悲。点额。嗯舔株,反正就這么個道理。

5.Websocket

WebSocket 規(guī)范定義了一種 API还棱,可在網(wǎng)絡(luò)瀏覽器和服務(wù)器之間建立“套接字”連接载慈。簡單地說:客戶端和服務(wù)器之間存在持久的連接,而且雙方都可以隨時開始發(fā)送數(shù)據(jù)珍手。詳細(xì)教程可以看 https://www.html5rocks.com/zh/tutorials/websockets/basics/

這種方式本質(zhì)沒有使用了 HTTP, 因此也沒有跨域的限制办铡,沒有什么過多的解釋直接上代碼吧。

前端部分

<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()」 方法可以安全地實現(xiàn)跨源通信琳要。通常寡具,對于兩個不同頁面的腳本,只有當(dāng)執(zhí)行它們的頁面位于具有相同的協(xié)議(通常為 https)稚补,端口號(443 為 https 的默認(rèn)值)童叠,以及主機 (兩個頁面的模數(shù) Document.domain設(shè)置為相同的值) 時,這兩個腳本才能相互通信课幕。「window.postMessage()」 方法提供了一種受控機制來規(guī)避此限制拯钻,只要正確的使用,這種方法就很安全撰豺。

用途

1.頁面和其打開的新窗口的數(shù)據(jù)傳遞

2.多窗口之間消息傳遞

3.頁面與嵌套的 iframe 消息傳遞

用法

詳細(xì)用法看 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

otherWindow.postMessage(message, targetOrigin, [transfer]);

  • otherWindow: 其他窗口的一個引用,比如 iframe 的 contentWindow 屬性拼余、執(zhí)行window.open返回的窗口對象污桦、或者是命名過或數(shù)值索引的window.frames。

  • message: 將要發(fā)送到其他 window 的數(shù)據(jù)匙监。

  • targetOrigin: 通過窗口的 origin 屬性來指定哪些窗口能接收到消息事件.

  • transfer(可選) : 是一串和 message 同時傳遞的 Transferable 對象. 這些對象的所有權(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>hello</div><script>  window.onmessage = e => {    console.log(e.data); // 秋風(fēng)的筆記    e.source.postMessage(e.data, e.origin);  };</script>

7.document.domain + Iframe

從第 7 種到第 9 種方式,我覺得別人的寫的已經(jīng)很好了亭姥,為了完整性稼钩,我就拿別人的了。如有雷同....(不對达罗,就是雷同....)不要說不出來坝撑。

「該方式只能用于二級域名相同的情況下,比如 a.test.comb.test.com 適用于該方式」粮揉。只需要給頁面添加 document.domain ='test.com' 表示二級域名都相同就可以實現(xiàn)跨域巡李。

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>

8.window.location.hash + Iframe

實現(xiàn)原理

原理就是通過 url 帶 hash ,通過一個非跨域的中間頁面來傳遞數(shù)據(jù)扶认。

實現(xiàn)流程

一開始 a.html 給 c.html 傳一個 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 對象的 name 屬性是一個很特別的屬性敞葛,當(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事件會觸發(fā)2次豺鼻,第1次加載跨域頁,并留存數(shù)據(jù)于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中數(shù)據(jù)      console.log(iframe.contentWindow.name);    }  }</script>

b.html 為中間代理頁,與 a.html 同域檩奠,內(nèi)容為空桩了。

// b.html<div></div>
// c.html<script>  window.name = "秋風(fēng)的筆記";</script>

通過 iframe 的 src 屬性由外域轉(zhuǎn)向本地域,跨域數(shù)據(jù)即由 iframe 的 window.name 從外域傳遞到本地域埠戳。這個就巧妙地繞過了瀏覽器的跨域訪問限制井誉,但同時它又是安全操作。

10.瀏覽器開啟跨域(終極方案)

其實講下其實跨域問題是瀏覽器策略整胃,源頭是他笔刹,那么能否能關(guān)閉這個功能呢淆九?

答案是肯定的逞壁。

「注意事項: 因為瀏覽器是眾多 web 頁面入口匆瓜。我們是否也可以像客戶端那種,就是用一個單獨的專門宿主瀏覽器蛮寂,來打開調(diào)試我們的開發(fā)頁面蔽午。例如這里以 chrome canary 為例,這個是我專門調(diào)試頁面的瀏覽器酬蹋,不會用它來訪問其他 web url及老。因此它也相對于安全一些。當(dāng)然這個方式范抓,只限于當(dāng)你真的被跨域折磨地崩潰的時候才建議使用以下写半。使用后,請以正常的方式將他打開尉咕,以免你不小心用這個模式干了其他的事叠蝇。」

Windows

找到你安裝的目錄.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx

Mac

~/Downloads/chrome-data 這個目錄可以自定義.

/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary  --disable-web-security --user-data-dir=~/Downloads/chrome-data

效果展示

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200413143102377</figcaption>

嗯,使用起來很香悔捶,但是再次提醒铃慷,一般情況千萬別輕易使用這個方式,這個方式好比七傷拳蜕该,使用的好威力無比犁柜,使用不好,很容易傷到自己堂淡。

三馋缅、為什么需要跨域?

在最一開始绢淀,我們知道了萤悴,跨域只存在于瀏覽器端。而瀏覽器為 web 提供訪問入口皆的。我們在可以瀏覽器內(nèi)打開很多頁面覆履。正是這樣的開放形態(tài),所以我們需要對他有所限制费薄。就比如林子大了硝全,什么鳥都有,我們需要有一個統(tǒng)一的規(guī)范來進行約定才能保障這個安全性楞抡。

1.限制不同源的請求

這里還是用最常用的方式來講解伟众,例如用戶登錄 a 網(wǎng)站,同時新開 tab 打開了 b 網(wǎng)站召廷,如果不限制同源赂鲤, b 可以像 a 網(wǎng)站發(fā)起任何請求,會讓不法分子有機可趁柱恤。

2.限制 dom 操作

我舉個例子吧, 你先登錄下 www.baidu.com ,然后訪問我這個網(wǎng)址。

https://zerolty.com/node-demo/index.html

image

<figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image-20200413190413758</figcaption>

你會發(fā)現(xiàn)找爱,這個和真實的百度一模一樣梗顺,如果再把域名搞的相似一些,是不是很容易被騙车摄,如果可以進行 dom 操作...那么大家的信息在這種釣魚網(wǎng)站眼里都是一顆顆小白菜寺谤,等著被收割。

?

可以在 http 返回頭 添加X-Frame-Options: SAMEORIGIN 防止被別人添加至 iframe吮播。

?

寫在最后

以上最常用的就是前 4 種方式变屁,特別是第 2 種非常常見,我里面也提到了多種示例意狠,大家可以慢慢消化一下粟关。希望未來有更加安全的方式來限制 web ,解決跨域的頭疼环戈,哈哈哈哈闷板。

「有一個不成熟的想法澎灸,可以搞這么一個瀏覽器,只讓訪問內(nèi)網(wǎng)/本地網(wǎng)絡(luò)遮晚,專門給開發(fā)者用來調(diào)試頁面用性昭,對于靜態(tài)資源可以配置白名單,這樣是不是就沒有跨域問題了县遣,23333糜颠。上述如有錯誤,請第一時間指出萧求,我會進行修改其兴,以免給大家來誤導(dǎo)。
參考
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

摘自微信公眾號-秋風(fēng)的筆記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饭聚,一起剝皮案震驚了整個濱河市忌警,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秒梳,老刑警劉巖法绵,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酪碘,居然都是意外死亡朋譬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門兴垦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徙赢,“玉大人,你說我怎么就攤上這事探越〗拼停” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵钦幔,是天一觀的道長枕屉。 經(jīng)常有香客問我,道長鲤氢,這世上最難降的妖魔是什么搀擂? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮卷玉,結(jié)果婚禮上哨颂,老公的妹妹穿的比我還像新娘。我一直安慰自己相种,他們只是感情好威恼,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般沃测。 火紅的嫁衣襯著肌膚如雪缭黔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天蒂破,我揣著相機與錄音馏谨,去河邊找鬼。 笑死附迷,一個胖子當(dāng)著我的面吹牛惧互,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播喇伯,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼喊儡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稻据?” 一聲冷哼從身側(cè)響起艾猜,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捻悯,沒想到半個月后匆赃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡今缚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年算柳,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姓言。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞬项,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出何荚,到底是詐尸還是另有隱情囱淋,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布餐塘,位于F島的核電站妥衣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏唠倦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一涮较、第九天 我趴在偏房一處隱蔽的房頂上張望稠鼻。 院中可真熱鬧,春花似錦狂票、人聲如沸候齿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慌盯。三九已至周霉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亚皂,已是汗流浹背俱箱。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留灭必,地道東北人狞谱。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像禁漓,于是被迫代替她去往敵國和親跟衅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359