跨域問題解決 方案

針對本文的九種方法我均寫的有相應的demo演示(對應的前端文件,後端文件和配置文件),強烈建議不熟悉的朋友都去嘗試一下。 本文github地址,fontService是前端地址文件,service是後端文件。

網絡上存在很多不同的跨域文章,我在學習的時候基本上也是去看他們的文章,但是有些地方的確理解起來有點困難,所以本文就這樣產生了,希望能寫一點和現在網絡上文章中都不一樣的東西。同時也把我自己的看法寫進去,和大家相互交流自己的看法。

跨域在以前一直折磨着每個前端開發者。但是現在,三大框架的普及,我們在開發的過程中,只修改小小的配置,便沒有這些顧慮。但實質上還是webpack-dev-server已經幫我們處理了這一個問題,把當前請求,代理到目標服務器上面,然後把獲取到的數據返回。所以,現在很多前端開發者,包括我在寫這篇文章之前,對跨域都不是很瞭解,只有一個個模模糊糊的概念,只會用jsonp去獲取,或者直接用jq的jsonp方法去解決跨域,都不會去了解爲什麼這樣就能解決跨域?甚至,很多人對跨域都已經放棄了,因爲三大框架的學習,完善的腳手架功能,簡化了我們,項目部署也有後端的同學幫我們解決。但是個人認爲跨域是每一個前端開發者需要必備的能力,不論是跨域獲取數據,還是後面進行ssr服務端渲染的配置都需要了解一點跨域,瞭解一點請求的代理。

爲什麼存在跨域

我們在解決的一個問題的同時我們應該先去了解這個問題是如何產生的。

之所以要使用跨域,最終的罪魁禍首都是瀏覽器的同源策略,瀏覽器的同源策略限制我們只能在相同的協議、IP地址、端口號相同,如果有任何一個不通,都不能相互的獲取數據。這裏注意一下,httphttps之間也存在跨域,因爲https一般採用的是443端口,http採用的是80端口或者其他。這裏也存在端口號的不同。想要詳細瞭解請看 => MDN對瀏覽器的同源策略的說明

雖然同源策略的確很可惡,但是如果沒有同源策略用戶將會陷入很境界。比如,你正在喫着火鍋哼着歌,逛着淘寶買東西,但是這時你的同學給你發了一個網址,然後你直接打開來看,假如沒有同源策略,他在該網站中用一個iframe的標籤然後把src指向淘寶網,這時沒有同源策略,他便可以直接通過js操作iframe的頁面,比如說獲取cookie或者用js模擬點擊這些操作(因爲你已經登錄過了,可以不用再次登錄就點擊了),危害是很大的。大家可以先去做一個小測驗,你在本地創建一個a.htmlb.html2個文件,然後在a.html中用iframe中插入b.html,你會發現當前2個頁面是一個域的,你可以在a中通過js控制b,和在b中直接用js操作沒有區別。但是如果你插入的不是同一個域下面的頁面,比如你插入的是淘寶,你會發現你不能通過js操作,你console.log(iframe.contentWindow)你會發現只有少數的幾項。大家可以去看看大佬的文章:淺談CSRF攻擊方式,雖然沒有永遠牢靠的盾,但是有同源策略的存在,會讓攻擊的成本變得高一點。

雖然同源策略危害很大,但是我們還是在一定的場景下面需要進行跨域處理,比如說百度全家桶,你在百度地圖和百度搜索2者之間肯定是放在2個域下面的(我沒有具體的去了解,但是我猜想肯定是這樣的)。在開發地圖的時候假如需要應用搜索的時候就不得不用跨域了。比如:百度搜索,輸入文字出現內容提示,如果我沒有判斷錯誤就是採用的jsonp來做得跨域。大家在學習了jsonp跨域的時候,可以去嘗試去獲取一下。

進入正題

1、JSONP

說起如何去解決跨域,我相信每個人腦袋中跳出來的第一個詞就是jsonp。因爲瀏覽器的同源策略不限制scriptlinkimg三個標籤,比如我們經常用這三個標籤來加載其他域的資源,我個人的看法這就已經算是跨域了。jsonp的跨域就是應用script標籤可以進行獲取遠程的js腳本文件。

// 比如1.js 腳本文件
say('haha');

我在html裏面引入1.js文件,那麼他講會執行say函數,我們需要傳入的數據就是haha

所以jsonp的方法就是動態的創建一個script標籤,然後設置src爲我們需要進行跨域的地址。當然這個方法需要後臺的設置。大家可以看我寫的代碼,

前端文件在fontEndService/www/demo1/index,html

btn.onclick = () => {
    jsonp('http://127.0.0.1:8888/api/getdata?jsonp=displayData');
}
function jsonp(url) {
    let script = document.createElement('script');
    script.setAttribute('src', url);
    document.getElementsByTagName('head')[0].appendChild(script);
}
function displayData(data) {
    msg.innerText = JSON.stringify(data);
}

然後後端代碼是用koa寫的一個簡約的接口,文件在service/app.js

// 簡約的後端代碼,我們直接調用前端傳過來的需要執行的函數
router.get('/api/getdata', ctx => {
  const params = get_params(ctx.request.url)
  const data = {
    title: '數據獲取',
    list: [0, 1, 2]
  }
  ctx.body = `${params.jsonp || 'callback'}(${JSON.stringify(data)})`;
})

前端通過script標籤給後臺發送一個get請求,在jsonp=displayData(一個我們接受到數據然後執行的方法,該方法是前端的),當我後臺接受到請求後,就返回一個,執行displayData這個方法的腳本。然後把我們需要傳遞的數據放在形參裏面。這樣就相當於我們在前端裏面執行displayData這個方法。用這個方法來實現跨域資源的共享。

此方法是比較常用的一個方法,簡單粗暴。但是此方法有一個致命的缺點就是隻支持GET請求。所以說如果前端頁面僅僅是作爲頁面的展示,就只獲取數據的話,只用此方法就沒有任何問題。

2、iframe+document.domain

這個方法,個人感覺不是特別的常用,因爲這個跨域方法要求2個域之間的主域名相同,子域不同,比如a.xxx.com和b.xxx.com。如果不同的話是不行的。

此方法的思想就是設置頁面的document.domain把他們設置成相同的域名,比如都設置成xxx.com。這樣來繞過同源策略。很簡單的一個方法,具體的代碼文件請看github。

代碼裏面的測試案列是,前端文件在7777端口,後臺文件在8888端口,前端如果需要請求後端的數據就存在跨域,所以我們在後端8888端口寫一個提供數據的中轉html,然後通過ajax或者其他的方法請求到數據,然後把數據往外暴露。此方法需要2個html都需要設置相同的主域。

3、iframe+location.hash

這種方法是一個很奇妙的方法,雖然我感覺很雞肋,但是它的實現方法很巧妙,我在學習的時候都感覺到不可思議,還能這麼玩?

首先我們需要了解hash是什麼?

比如有一個這樣的url:http://www.xxx.com#abc=123,那麼我們通過執行location.hash就可以得到這樣的一個字符串#abc=123,同時改變hash頁面是不會刷新的。這一點,相信很多學習三大框架的朋友應該都見識過hash路由。所以說我們可以根據這一點來在#後面加上我們需要傳遞的數據。

加入現在我們有A頁面在7777端口(前端顯示的文件),B頁面在8888端口,後臺運行在8888端口。我們在A頁面中通過iframe嵌套B頁面。

1、從A頁面要傳數據到B頁面

我們在A頁面中通過,修改iframesrc的方法來修改hash的內容。然後在B頁面中添加setInterval事件來監聽我們的hash是否改變,如果改變那麼就執行相應的操作。比如像後臺服務器提交數據或者上傳圖片這些。

 

2、從B頁面傳遞數據到A頁面

經過上面的方法,那麼肯定有聰明的朋友就在想那麼,從B頁面向A頁面發送數據就是修改A頁面的hash值了。對沒錯方法就是這樣,但是我在執行的時候會出現一些問題。我們在B頁面中直接:

 

parent.location.hash = "#xxxx"

 

這樣是不行的,因爲前面提到過的同源策略不能直接修改父級的hash值,所以這裏採用了一個我認爲很巧妙的方法。部分代碼:

 

try {
    parent.location.hash = `message=${JSON.stringify(data)}`;
} catch (e) {
    // ie、chrome 的安全機制無法修改parent.location.hash,
    // 利用一箇中間html 的代理修改location.hash
    // 如A => B => C 其中,當前頁面是B,AC在相同的一個域下。B 不能直接修改A 的 hash值故修改 C,讓C 修改A
    // 文件地址: fontEndService/www/demo3/proxy.html
    if (!ifrProxy) {
        ifrProxy = document.createElement('iframe');
        ifrProxy.style.display = 'none';
        document.body.appendChild(ifrProxy);
    }
    ifrProxy.src = `http://127.0.0.1:7777/demo3/proxy.html#message=${JSON.stringify(data)}`;
}

 

如果可以直接修改我們就直接修改,如果不能直接修改,那麼我們在B頁面中再添加一個iframe然後指向C頁面(我們暫時叫他代理頁面,此頁面和A頁面是在相同的一個域下面),我們可以用同樣的方法在url後面我們需要傳遞的信息。在代理頁面中:

 

parent.parent.location.hash = self.location.hash.substring(1);

 

只需要寫這樣的一段js代碼就完成了修改A頁面的hash值,同樣在A頁面中也添加一個setInterval事件來監聽hash值的改變。

 

我們現在再來理一下思路。我們現在有三個頁面,A,B,C。

 

A頁面是我們前端顯示的頁面;

B頁面我們可以把他當做A頁面也後端數據交互的一箇中間頁面,完成接受A的數據和向後臺發送請求。但是由於同源策略的限制我們不能在B頁面中直接修改A的hash值,所以我們需要藉助一個與A頁面在同一個域名下的C頁面。

C頁面我們把他當中一個代理頁面,我們因爲他和A頁面在一個域下,所以可以修改A的hash值。所以B頁面修改C頁面的hash值,然後C頁面修改A頁面的hash值。(C就是一個打工的)

此方法雖然我個人感覺實現的思路很巧妙但是,使用價值似乎不高,因爲他實現的核心思路就是通過修改URL的hash值,然後用定時器來監聽值的改變來修改。所以說最大的問題就是,我們傳遞的數據會直接在URL裏面顯示出來,不是很安全,同時URL的長度是一定的所以傳輸的數據也是有限的。

4、iframe+window.name

相比於前面2種iframe的方法,這種方法的使用人數要多一點。因爲他有效的解決了前面2種方法很大的缺點。這種方法的原理就是window.name屬性在於加載不同的頁面(包括域名不同的情況下),如果name值沒有修改,那麼它將不會變化。並且這個值可以非常的長(2MB)

方法原理:A頁面通過iframe加載B頁面。B頁面獲取完數據後,把數據賦值給window.name。然後在A頁面中修改iframe使他指向本域的一個頁面。這樣在A頁面中就可以直接通過iframe.contentWindow.name獲取到B頁面中獲取到的數據。

A頁面中部分代碼:

let mark = false;
let ifr = document.createElement('iframe');
ifr.src = "http://127.0.0.1:8888/demo4";
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = () => {
    // iframe 中數據加載完成,觸發onload事件
    if (mark) {
        msg.innerText = ifr.contentWindow.name;// 這就是數據
        document.body.removeChild(ifr);
        mark = false;
    } else {
        mark = true;
        // 修改src指向本域的一個頁面(這個頁面什麼都沒有)
        ifr.contentWindow.location = "http://127.0.0.1:7777/demo4/proxy.html";
    }
}

 

5、postMessage

postMessage是HTML5引入的API。他可以解決多個窗口之間的通信(包括域名的不同)。我個人認爲他算是一種消息的推送,可以給每個窗口推送。然後在目標窗口添加message的監聽事件。從而獲取推送過來的數據。

A頁面中部分代碼:

<iframe id="iframe" src="http://127.0.0.1:8888/demo5" frameborder="0"></iframe>

iframe.contentWindow.postMessage('getData', 'http://127.0.0.1:8888');

// 監聽其他頁面給我發送的數據
window.addEventListener('message', e => {
    if (e.origin !== 'http://127.0.0.1:8888') return;
    msg.innerText = e.data;
})

這裏我們給目標窗口127.0.0.1:8888推送了getData的數據。然後在B頁面中添加事件的監聽。

B頁面中部分代碼:

window.addEventListener('message', e => {
    // 判斷來源是不是我們信任的站點,防止被攻擊
    if (e.origin !== 'http://127.0.0.1:7777') return;
    const data = e.data;
    if (data === 'getData') {
        // 根據接受到的數據,來進行下一步的操作
        myAjax('/api/getdata', notice);
    }
})
function notice(data) {
    // 向後臺請求到數據以後,在向父級推送消息
    top.postMessage(JSON.stringify(data), 'http://127.0.0.1:7777')
}

我個人認爲這種方式就是一個事件的發佈與監聽,至少說可以無視同源策略。

6、cors

其實對於跨域資源的請求,瀏覽器已經把我們的請求發放給了服務器,瀏覽器也接受到了服務器的響應,只是瀏覽器一看我們2個的域不一樣就把消息給攔截了,不給我們顯示。所以我們如果我們在服務器就告訴瀏覽器我這個數據是每個源都可以獲取就可以了。這就是CORS跨域資源共享。

在後臺代碼中我以KOA列子

const Koa = require('koa');
const router = require('koa-router')();
// 引入中間件
const cors = require('koa2-cors');

const app = new Koa();

// 根據後臺服務器的類型,打開跨域設置
// cors安全風險很高,所以,實際線上的配置肯定要比這個更加複雜,需要根據自己的需求來做
app.use(cors());

router.get('/api/getdata', ctx => {
  ctx.body = {
    code: 200,
    msg: '我是配置有cors的服務器傳輸的數據'
  }
})

app.use(router.routes(), router.allowedMethods());

console.log('配置有cors的服務器地址在:http://127.0.0.1:8889');
app.listen(8889);

這樣的話,任何源都可以通過AJAX發起請求來獲取我們提供的數據。針對不同語言的服務器後端有不一樣的處理方法,但是實質是一樣的。

配置了CORS的瀏覽器請求響應信息

跨域請求響應信息

 

7、NGINX

採用nginx做代理應該是目前跨域解決方案最好的一種。現在強調前後端分離,前端根據後端提供的接口進行數據的交互然後渲染頁面。在前端三大框架的同時,開發過程中不需要我們針對跨域配置很多。在網頁上線以後。我們經常採用nginx來加載靜態的資源,我們把我們前端打包好的文件放到nginx的目錄下面,讓nginx來處理客服端的靜態資源的請求。然後後端部署到另外一個端口號上面,當我們需要進行數據的交互的時候,通過nginx代理把後端數據代理到前端頁面。這樣的步驟是相較於傳統的跨域是最簡單也是最有效的一種方法,因爲nginx又沒有同源策略。不用考慮什麼兼容性也不用考慮數據大小。我們在服務器(或者測試代碼的時候在本地)安裝nginx服務,然後找到我們nginx的配置文件,添加以下配置文件:

server {  # 把頁面部署的端口
  listen 8080;  # 靜態頁面存放的目錄
  root /var/www/html; 
  index  index.html index.htm index.php;  # 只代理 /api 開頭的接口,其他接口不代理
  location /api/ {    # 需要代理的地址, 輸入我們的後臺api地址
    proxy_pass http://127.0.0.1:8888;
  }
}

這樣,我們可以代理不同url開頭的請求到不同的後端進行處理,對以後服務器做負載均衡和反向代理也很簡單。

8、nodejs

其實這種辦法和上一種用nginx的方法是差不多的。都是你把請求發給一箇中間人,由於中間人沒有同源策略,他可以直接代理或者通過爬蟲或者其他的手段得到想到的數據,然後返回(是不是和VPN的原理有點類似)。

當然現在常見的就是用nodejs作爲數據的中間件,同樣,不同的語言有不同的方法,但是本質是一樣的。我上次自己同構自己的博客頁面,用react服務器端渲染,因爲瀏覽器的同源策略,請求不到數據,然後就用了nodejs作爲中間件來代理請求數據。

部分代碼:

const Koa = require('koa');
// 代理
const Proxy = require('koa-proxy');
// 對以前的異步函數進行轉換
const Convert = require('koa-convert');

const app = new Koa();
const server = require('koa-static');
app.use(server(__dirname+"/www/",{ extensions: ['html']}));

app.use(Convert(Proxy({
  // 需要代理的接口地址
  host: 'http://127.0.0.1:8888',
  // 只代理/api/開頭的url
  match: /^\/api\//
})));

console.log('服務運行在:http://127.0.0.1:7777');
app.listen(7777);

是不是和nginx很類似呀!!

9、webSocket

webSocket大家應該都有所耳聞,主要是爲了客服端和服務端進行全雙工的通信。但是這種通信是可以進行跨端口的。所以說我們可以用這個漏洞來進行跨域數據的交互。

我個人認爲,這種方法實質上和postMessage差不多,但是不是特別的常用吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章