「深入淺出」前端開發(fā)中常用的幾種跨域解決方案
看完本文可以系統(tǒng)地掌握,不同種跨域解決方案間的巧妙,以及它們的用法、原理、局限性和適用的場景
包括以下幾個(gè)方面:
跨域的現(xiàn)象,和幾種常見的跨域表現(xiàn)
跨域的解決方案(原理分析)
修改本地HOST
JSONP
CORS
Proxy
Nginx反向代理
Post Message(利用iframe標(biāo)簽,實(shí)現(xiàn)不同域的關(guān)聯(lián))
同源是什么?
如果兩個(gè)URL的協(xié)議protocol、主機(jī)名host和端口號port都相同的話,則這兩個(gè)URL是同源。
同源策略
同源策略是一個(gè)重要的安全策略。它能夠阻斷惡意文檔,減少被攻擊的媒介。
真實(shí)項(xiàng)目中,很少有同源策略,大部分都是非同源策略
跨域是什么?
當(dāng)協(xié)議、域名與端口號中任意一個(gè)不相同時(shí),都算作不同域,不同域之間相互請求資源的表現(xiàn)(非同源策略請求),稱作”跨域“。
跨域現(xiàn)象
那么我們就下面的網(wǎng)址分析一下,哪一塊是協(xié)議,哪一塊是域名及端口號
- http://kbs.sports.qq.com/index.html
- 協(xié)議:http(還有以一種https協(xié)議)
- 域名:kbs.sports.qq.com
- 端口號:80
- https://127.0.0.1:3000
- 協(xié)議:https
- 域名:127.0.0.1
- 端口號:3000
假如我們的真實(shí)項(xiàng)目開發(fā)中的Web服務(wù)器地址為 ”http://kbs.sports.qq.com/index.html“,而需要請求的數(shù)據(jù)接口地址為 "http://api.sports.qq.com/list"。
當(dāng)Web服務(wù)器的地址向數(shù)據(jù)接口的地址發(fā)送請求時(shí),便會(huì)造成了跨域現(xiàn)象
造成跨域的幾種常見表現(xiàn)
服務(wù)器分開部署(Web服務(wù)器 + 數(shù)據(jù)請求服務(wù)器)
本地開發(fā)(本地預(yù)覽項(xiàng)目 調(diào)取 測試服務(wù)器的數(shù)據(jù))
調(diào)取第三方平臺(tái)的接口
Web服務(wù)器:主要用來靜態(tài)資源文件的處理
解決方案
- 修改本地HOST(不作介紹)
- JSONP
- CORS
- Proxy
- Nginx反向代理
- Post Message(利用iframe標(biāo)簽,實(shí)現(xiàn)不同域的關(guān)聯(lián))
在后面會(huì)詳細(xì)分析這四種解決方案的原理和用法配置,以及它們的優(yōu)點(diǎn)和局限性
- 注意: 基于ajax或fetch發(fā)送請求時(shí),如果是跨域的,則瀏覽器默認(rèn)的安全策略會(huì)禁止該跨域請求
- 補(bǔ)充說明:以下所有的測試用例,均由Web:http://127.0.0.1:5500/index.html向API:http://127.0.0.1:1001/list發(fā)起請求
API接口的服務(wù)器端是自己通過express建立的,下文在服務(wù)器端以app.use中間件的形式接受來自客戶端的請求并做處理。
- 即 在“http://127.0.0.1:1001/list”from origin“http://127.0.0.1:55”上對XMLHttpRequest的訪問已被CORS策略阻止:被請求的資源上沒有“Access- control - allow-origin”頭
在后端開啟了一個(gè)端口號為1001的服務(wù)器之后,我們來實(shí)踐一下
- let xhr = new XMLHttpRequest;
- xhr.open('get', 'http://127.0.0.1:1001/list');
- xhr.onreadystatechange = () => {
- if (xhr.status === 200 && xhr.readyState === 4) {
- console.log(xhr.responseText);
- }
- };
- xhr.send();
跨域的常見報(bào)錯(cuò)提示
這就是由于瀏覽器默認(rèn)的安全策略禁止導(dǎo)致的。
下面介紹一下幾種常見的解決方案。
JSONP
原理:JSONP利用script標(biāo)簽不存在域的限制,且定義一個(gè)全局執(zhí)行上下文中的函數(shù)func
(用來接收服務(wù)器端返回的數(shù)據(jù)信息)來接收數(shù)據(jù),從而實(shí)現(xiàn)跨域請求。
- 弊端:
- 只允許GET請求
- 不安全:只要瀏覽器支持,且存在瀏覽器的全局變量里,則誰都可以調(diào)用
圖解JSONP的原理
手動(dòng)封裝JSONP
callback必須是一個(gè)全局上下文中的函數(shù)
(防止不是全局的函數(shù),我們需要把這個(gè)函數(shù)放在全局上,并且從服務(wù)器端接收回信息時(shí),要瀏覽器執(zhí)行該函數(shù))
注意:
uniqueName變量存儲(chǔ)全局的回調(diào)函數(shù)(確保每次的callback都具有唯一性)
檢驗(yàn)url中是否含有"?",有的話直接拼接callback,沒有的話補(bǔ)”?“
- // 客戶端
- function jsonp(url, callback) {
- // 把傳遞的回調(diào)函數(shù)掛載到全局上
- let uniqueName = `jsonp${new Date().getTime()}`;
- // 套了一層 anonymous function
- // 目的讓 返回的callback執(zhí)行且刪除創(chuàng)建的標(biāo)簽
- window[uniqueName] = data => {
- // 從服務(wù)器獲取結(jié)果并讓瀏覽器執(zhí)行callback
- document.body.removeChild(script);
- delete window[uniqueName];
- callback && callback(data);
- }
- // 處理URL
- url += `${url.includes('?')} ? '&' : '?}callback=${uniqueName}'`;
- // 發(fā)送請求
- let script = document.createElement('script');
- script.src = url;
- document.body.appendChild(script);
- }
- // 執(zhí)行第二個(gè)參數(shù) callback,獲取數(shù)據(jù)
- jsonp('http://127.0.0.1:1001/list?userName="lsh"', (result) => {
- console.log(result);
- })
- // 服務(wù)器端
- // Api請求數(shù)據(jù)
- app.get('/list', (req, res) => {
- // req.query 問號后面?zhèn)鬟f的參數(shù)信息
- // 此時(shí)的callback 為傳遞過來的函數(shù)名字 (uniqueName)
- let { callback } = req.query;
- // 準(zhǔn)備返回的數(shù)據(jù)(字符串)
- let res = { code: 0, data: [10,20] };
- let str = `${callback}($(JSON.stringify(res)))`;
- // 返回給客戶端數(shù)據(jù)
- res.send(str);
- })
測試用例展示:
客戶端請求的url
服務(wù)端返回的數(shù)據(jù)
返回的callback
返回的數(shù)據(jù)信息 result
- // 服務(wù)器請求的 url
- Request URL:
- http://127.0.0.1:1001/list?userName="lsh"&callback=jsonp159876002
- // 服務(wù)器返回的函數(shù)callback
- jsonp159876002({"code":0, "data": [10,20]});
- // 客戶端接收的數(shù)據(jù)信息
- { code: 0, data: Array(2) }
當(dāng)瀏覽器發(fā)現(xiàn)返回的是jsonp159876002({"code":0, "data": [10,20]});這個(gè)函數(shù),會(huì)自動(dòng)幫我們執(zhí)行的。
JSONP弊端
在上文中說到只要服務(wù)器端那里設(shè)置了允許通過jsonp的形式跨域請求,我們就可以取回?cái)?shù)據(jù)。
下面是在我們封裝完jsonp方法之后,向一個(gè)允許任何源向該服務(wù)器發(fā)送請求的網(wǎng)址xxx
- jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => {
- console.log(result);
- });
CORS
上文提到,不允許跨域的根本原因是因?yàn)锳ccess-Control-Allow-Origin已被禁止
那么只要讓服務(wù)器端設(shè)置允許源就可以了
原理:解決掉瀏覽器的默認(rèn)安全策略,在服務(wù)器端設(shè)置允許哪些源請求就可以了
先來看一下下面的設(shè)置有哪些問題
- // 服務(wù)器端
- app.use((req, res, next) => {
- // * 允許所有源(不安全/不能攜帶資源憑證)
- res.header("Access-Control-Allow-Origin", "*");
- res.header("Access-Control-Allow-Credentials", true);
- /* res.header("Access-Control-Allow-Headers", "Content-Type,....");
- res.header("Access-Control-Allow-Methods", "GET,..."); */
- // 試探請求:在CORS跨域請求中,首先瀏覽器會(huì)自己發(fā)送一個(gè)試探請求,驗(yàn)證是否可以和服務(wù)器跨域通信,服務(wù)器返回200,則瀏覽器繼續(xù)發(fā)送真實(shí)的請求
- req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
- });
- // 客戶端
- let xhr = new XMLHttpRequest;
- xhr.open('get', 'http://127.0.0.1:1001/list');
- xhr.setRequestHeader('Cookie', 'name=jason');
- xhr.withCredentials = true;
- xhr.onreadystatechange = () => {
- if (xhr.status === 200 && xhr.readyState === 4) {
- console.log(xhr.responseText);
- }
- };
- xhr.send();
當(dāng)我們一旦在服務(wù)器端設(shè)置了允許任何源可以請求之后,其實(shí)請求是不安全的,并且要求客戶端不能攜帶資源憑證(比如上文中的Cookie字段),瀏覽器端會(huì)報(bào)錯(cuò)。
告訴我們Cookie字段是不安全的也不能被設(shè)置的,如果允許源為'*'的話也是不允許的。
假如在我們的真實(shí)項(xiàng)目開發(fā)中
正確寫法✅
設(shè)置單一源(安全/也可以攜帶資源憑證/只能是單一一個(gè)源)
也可以動(dòng)態(tài)設(shè)置多個(gè)源:每一次請求都會(huì)走這個(gè)中間件,我們首先設(shè)置一個(gè)白名單,如果當(dāng)前客戶端請求的源在白名單中,我們把Allow-Origin動(dòng)態(tài)設(shè)置為當(dāng)前這個(gè)源
- app.use((req, res, next) => {
- // 也可自定義白名單,檢驗(yàn)請求的源是否在白名單里,動(dòng)態(tài)設(shè)置
- /* let safeList = [
- "http://127.0.0.1:5500",
- xxx,
- xxxxx,
- ]; */
- res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
- res.header("Access-Control-Allow-Credentials", true); // 設(shè)置是否可攜帶資源憑證
- /* res.header("Access-Control-Allow-Headers", "Content-Type,....");
- res.header("Access-Control-Allow-Methods", "GET,..."); */
- // 試探請求:在CORS跨域請求中,首先瀏覽器會(huì)自己發(fā)送一個(gè)試探請求,驗(yàn)證是否可以和服務(wù)器跨域通信,服務(wù)器返回200,則瀏覽器繼續(xù)發(fā)送真實(shí)的請求
- req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
- });
CORS的好處
原理簡單,容易配置,允許攜帶資源憑證
仍可以用 ajax作為資源請求的方式
可以動(dòng)態(tài)設(shè)置多個(gè)源,通過判斷,將Allow-Origin設(shè)置為當(dāng)前源
CORS的局限性
只允許某一個(gè)源發(fā)起請求
如多個(gè)源,還需要?jiǎng)討B(tài)判斷
Proxy
Proxy翻譯為“代理”,是由webpack配置的一個(gè)插件,叫"webpack-dev-server"(只能在開發(fā)環(huán)境中使用)
Proxy在webpack中的配置
- const path = require('path');
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- module.exports = {
- mode: 'production',
- entry: './src/main.js',
- output: {...},
- devServer: {
- port: '3000',
- compress: true,
- open: true,
- hot: true,
- proxy: {
- '/': {
- target: 'http://127.0.0.1:3001',
- changeOrigin: true
- }
- }
- },
- // 配置WEBPACK的插件
- plugins: [
- new HtmlWebpackPlugin({
- template: `./public/index.html`,
- filename: `index.html`
- })
- ]
- };
圖解Proxy的原理
Proxy代理其實(shí)相當(dāng)于由webpack-dev-server配置在本地創(chuàng)建了一個(gè)port=3000的服務(wù),利用node的中間層代理(分發(fā))解決了瀏覽器的同源策略限制。
但是它只能在開發(fā)環(huán)境下使用,因?yàn)閐ev-server只是一個(gè)webpack的一個(gè)插件;
如果需要在生產(chǎn)環(huán)境下使用,需要我們配置Nginx反向代理服務(wù)器;
另外如果是自己實(shí)現(xiàn)node服務(wù)層代理:無論是開發(fā)環(huán)境還是生產(chǎn)環(huán)境都可以處理(node中間層和客戶端是同源,中間層幫助我們向服務(wù)器請求數(shù)據(jù),再把數(shù)據(jù)返回給客戶端)
Proxy的局限性
只能在本地開發(fā)階段使用
配置Nginx反向代理
主要作為生產(chǎn)環(huán)境下跨域的解決方案。
原理:利用Node中間層的分發(fā)機(jī)制,將請求的URL轉(zhuǎn)向服務(wù)器端的地址
配置反向代理
- server {
- listen: 80;
- server_name: 192.168.161.189;
- loaction: {
- proxy_pass_http://127.0.0.1:1001; // 請求轉(zhuǎn)向這個(gè)URL地址,服務(wù)器地址
- root html;
- index index.html;
- }
- }
簡單寫了一下偽代碼,實(shí)際開發(fā)中根據(jù)需求來配。
POST MESSAGE
假設(shè)現(xiàn)在有兩個(gè)頁面,分別為A頁面port=1001、B頁面port=1002,實(shí)現(xiàn)頁面A與頁面B的頁面通信(跨域)
原理:
把 B頁面當(dāng)做A的子頁面嵌入到A頁面里,通過iframe.contentWindow.postMessage向B頁面?zhèn)鬟f某些信息
在A頁面中通過window.onmessage獲取A頁面?zhèn)鬟f過來的信息ev.data(見下代碼)
同理在B頁面中通過ev.source.postMessage向A頁面?zhèn)鬟f信息
在A頁面中通過window.onmessage獲取B頁面?zhèn)鬟f的信息
主要利用內(nèi)置的postMessage和onmessage傳遞信息和接收信息。
A.html
- // 把 B頁面當(dāng)做A的子頁面嵌入到A頁面里
- <iframe id="iframe" src="http://127.0.0.1:1002/B.html" frameborder="0" style="display: none;"></iframe>
- <script>
- iframe.onload = function () {
- iframe.contentWindow.postMessage('珠峰培訓(xùn)', 'http://127.0.0.1:1002/');
- }
- //=>監(jiān)聽B傳遞的信息
- window.onmessage = function (ev) {
- console.log(ev.data);
- }
- </script>
B.html
- <script>
- //=>監(jiān)聽A發(fā)送過來的信息
- window.onmessage = function (ev) {
- // console.log(ev.data);
- //=>ev.source:A
- ev.source.postMessage(ev.data + '@@@', '*');
- }
- </script>
幾種方案的比較
1. JSONP方案需要前后端共同配置完成(利用script標(biāo)簽不存在域的限制)【麻煩,老項(xiàng)目使用】
2. CORS原理簡單,但只能配置單一源,如果需要配置多個(gè)源,也只能從白名單中篩選出某一個(gè)符合表求的origin【偶爾使用】
服務(wù)器端需要單獨(dú)做處理,客戶端較為簡單
3. Proxy客戶端通過dev-server,生產(chǎn)環(huán)境需要配置Nginx反向代理(利用Node中間層分發(fā)機(jī)制)【常用】