走過路過來看看各種實現跨域的方式~

凌晨夏沫發表於2018-06-06

跨域是什麼?

說到跨域我們先來說說同源,同源是指"協議+域名+埠"三者相同,所謂的跨域就是跨協議跨域名跨埠

跨域導致的問題

由於安全原因,跨域訪問是被各大瀏覽器所禁止的。

Cookie、LocalStorage 和 IndexDB 無法跨域讀取

假如你登入了某個銀行網站那麼該網站會傳送cookie作為登入憑證。假如此時你訪問了惡意網站,對其傳送請求如果可以攜帶cookie跨域,那麼cookie會傳送到惡意網站,此時就可以模擬使用者去銀行網站傳送請求。


DOM同源策略也一樣,不能通過iframe引入其他域下的頁面直接操作其dom元素

在自己的網站嵌入別人的網站,如果嵌入的網站有登入功能,如果可以跨域獲得嵌入頁面的元素,那在我們的網站上可以輕鬆獲取到使用者的賬號密碼。


ajax請求不能跨域傳送

如果不限制ajax跨域傳送資料,就相當於網站的所有介面對所有人都是公開的


跨域解決方案

一.jsonp

jsonp主要是利用標籤的src屬性沒有受同源策略限制來獲取來自其他域名的資料。我們通常使用 img/script 標籤。

先舉個例子~ (bd搜尋框) 在訪問bd進行搜尋時我們可以很輕鬆的獲取搜尋介面

https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=hello&cb=list

介面返回的結果

// 不難發現cb傳什麼返回的就是什麼
list({q:"hello",p:false,s:["hello kitty","hello kitty樂園","hello 樹先生","hello world","hellobike","hello tv","hello女神","hellotalk","hello語音","hello venus"]});
複製程式碼

開始來寫我們的例子

<script>
  function list(data){
    console.log(data);
  } 
</script>
<script src="https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=hello&cb=list"></script>
複製程式碼

好了這就已經實現了跨域了,我們訪問了bd的介面!

走過路過來看看各種實現跨域的方式~

加深點印象我們自己來寫下服務端吧!

// client
<script>
  function list(data){
    console.log(data);
  } 
</script>
<script src="http://localhost:3000/getData?cb=list"></script>
// server
let express = require('express');
let app = express();
app.get('/getData', (req,res) => {
  let cbName = req.query.cb
  res.end(`${cbName}({name:'jw'})`);
});
app.listen(3000);
複製程式碼

缺點:它只支援GET請求而不支援POST等其它型別的HTTP請求,可能會導致xss攻擊

二.CORS

CORS(Cross-Origin Resource Sharing, 跨源資源共享)是W3C出的一個標準,想要實現跨域主要靠伺服器進行一些設定。客戶端不用做任何更改!

先列點雜七雜八的東西,後面我們會用到!
Access-Control-Allow-Origin 允許的域
Access-Control-Allow-Credentials 允許攜帶cookie
Access-Control-Expose-Headers 允許客戶端獲取哪個頭
Access-Control-Allow-Methods 允許的方法
Access-Control-Allow-Headers 允許哪些頭
Access-Control-Max-Age 預檢請求的結果快取

廢話不多說先上例子~啟動兩個服務

// 通過3000埠來啟動index.html
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);

// 4000埠有getData介面
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.get('/getData', (req,res) => {
  console.log(req.headers);
  res.end('{name:"jw"}')
});
app.listen(4000);
複製程式碼

在html傳送ajax請求

let xhr = new XMLHttpRequest();
xhr.open('get','/getData',true);
xhr.onreadystatechange = function () {
  if(xhr.readyState === 4){
    if(xhr.status >=200 && xhr.status<300|| xhr.status ===304){
      console.log(xhr.response)
    }
  }
}  
xhr.send();
複製程式碼

head1
head1
我們可以發現雖然沒有獲取到資料但是4000埠其實是接收到了響應的,只是瀏覽器預設遮蔽了響應結果

第一次改造服務端:

當請求傳送過來時,要根據當前客戶端的origin屬性判斷是否允許跨域

let express = require('express');
let app = express();
app.use(express.static(__dirname));
let whiteList = ['http://localhost:3000'];
app.use(function (req,res,next) {
  if (whiteList.includes(req.headers.origin)) {
    // 設定某個域可以允許訪問,也可以使用* 但是不建議這樣做
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
  }
  next();
})
app.get('/getData', (req,res) => {
  console.log(req.headers);
  res.end('{name:"jw"}');
});
app.listen(4000);
複製程式碼

預設跨域是不攜帶cookie的,可是我想帶!強制設定攜帶cookie

// client
document.cookie = 'a=1'
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
複製程式碼

head1

第二次改造服務端:

app.use(function (req,res,next) {
  if (whiteList.includes(req.headers.origin)) {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
+   res.setHeader('Access-Control-Allow-Credentials',true)
  }
  next();
})
複製程式碼

客戶端還是不滿意想在傳遞些自定義的頭!

document.cookie = 'a=1'
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('get','http://localhost:4000/getData',true);
xhr.setRequestHeader('name', 'jw');
複製程式碼

head1

第三次改造服務端:

app.use(function (req,res,next) {
  if (whiteList.includes(req.headers.origin)) {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Access-Control-Allow-Credentials',true);
+   res.setHeader('Access-Control-Allow-Headers',"name");
  }
  next();
});
複製程式碼

客戶端又發現傳送put請求時。又出現了噁心的問題......

xhr.open('put','http://localhost:4000/getData',true);
複製程式碼

head1
這回我一眼就看到了問題,肯定是沒有設定允許接收的方法

第四次改造服務端:

app.use(function (req,res,next) {
  if (whiteList.includes(req.headers.origin)) {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Access-Control-Allow-Credentials',true);
    res.setHeader('Access-Control-Allow-Headers',"name");
+   res.setHeader('Access-Control-Allow-Methods','PUT');
  }
  next();
});
app.put('/getData', (req,res) => {
  console.log(req.headers);
  res.end('{name:"jw"}');
});
複製程式碼

等等怎麼多了個請求?

head1

原來是這樣對於非簡單請求,會在正式通訊之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。型別為 options。

第五次改造服務端:

app.use(function (req,res,next) {
  if (whiteList.includes(req.headers.origin)) {
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Access-Control-Allow-Credentials',true);
    res.setHeader('Access-Control-Allow-Headers',"name");
    res.setHeader('Access-Control-Allow-Methods','PUT');
    // 設定預檢查傳送的時間
+   res.setHeader('Access-Control-Max-Age',6000);
    if (req.method === 'OPTIONS'){
      res.end();
    }else{
      next();
    }
  }
});
複製程式碼

到此客戶端終於不搞事了。服務端為了表示友好給他寫了個頭

app.put('/getData', (req,res) => {
  res.setHeader('info','ok');
  res.end('{name:"jw"}');
});
複製程式碼
xhr.onreadystatechange = function () {
  if(xhr.readyState === 4){
    if(xhr.status >=200 && xhr.status<300|| xhr.status ===304){
+     console.log(xhr.getResponseHeader('info'));
      console.log(xhr.response);
    }
  }
}  
複製程式碼

head1

這才發現客戶端原來是收不到的,好吧!還需要告訴客戶端可以拿到這個值

app.put('/getData', (req,res) => {
  res.setHeader('info', 'ok');
  res.setHeader('Access-Control-Expose-Headers', 'info');
  res.end('{name:"jw"}');
});
複製程式碼

到此! 終於知道cors是個什麼鬼了,就是設定各種約定好的頭

CORS 不僅使用方便,支援所有型別請求,具有許可權控制,而且瀏覽器原生支援,我們可以輕易的處理請求異常。利於排查錯誤。所以我們大多數情況下會首選該方式。在低版本瀏覽器可以使用jsonp配合其他方式來相容。

三.postMessage

在來看看兩個視窗是如何實現跨域的,先看看H5的postMessage! postMessage()方法可以安全地實現跨源通訊

現在的需求是這樣的 http://127.0.0.1:3000/a.html 想對http://127.0.0.1:4000/b.html 說我愛你,之後b頁面收到訊息後對其說我不愛你~,這事多麼悲傷的一個故事#_#。

// a.html
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()" frameborder="0"></iframe>
<script>
  function load() {
    let frame = document.getElementById('frame');
    frame.contentWindow.postMessage('我愛你','http://localhost:4000');
    window.onmessage = function (e) {
      console.log(e.data);
    }
  }
</script>

// b.html
<script>
  window.onmessage = function (e) {
    console.log(e.data);
    e.source.postMessage('我不愛你',e.origin);
  }  
</script>
複製程式碼

四.document.domain

domain方式跨域要先保證兩個頁面屬於同一個域下的 我們在hosts檔案增加兩個對映關係,位置在 C:\Windows\System32\drivers\etc

127.0.0.1 a.fullstackjavascript.cn
127.0.0.1 b.fullstackjavascript.cn
複製程式碼
// a.html
<iframe src="http://b.fullstackjavascript.cn:4000/b.html" 
        frameborder="0" id="frame"
        onload="load()"
></iframe>
<script>
  function load() {
    document.domain = 'fullstackjavascript.cn';
    console.log(frame.contentWindow.name);
  }
</script>

// b.html
<script>
  document.domain = 'fullstackjavascript.cn';
  var name = 'jw';  
</script>
複製程式碼

訪問http://a.fullstackjavascript.cn:3000/a.html,發現是可以拿到另一個頁面中的值

五.location.hash

用hash傳遞資料就比較有意思了,需要藉助第三個頁面實現。
先用粗俗的話介紹一下,先有三個頁面hash1,hash2,hash3他們之間的關係是hash1和hash2是同域下的hash3是獨立域下的,我們通過hash1頁面用iframe引入hash3頁面,可以在引入時給hash3頁面傳遞hash值,hash3接到hash值後算出需要返回結果,在建立iframe引入hash2把結果通過hash的方式傳遞給hash2,hash2和hash1是同域的,hash2可以直接操控hash1的值,此時hash1頁面可以監控hash值的變化來獲取hash2的資料

// hash1.html
<iframe src="http://a.fullstackjavascript.cn:3000/hash3.html#iloveyou" frameborder="0"></iframe>
<script>
    window.onhashchange = function () {
      console.log(location.hash);
    }
</script>

// hash2.html
<script>
  let hash = location.hash;
  let data;
  if(hash === '#iloveyoue'){
    data = 'idontloveyou'
  }
  let frame = document.createElement('iframe');
  frame.src = `http://a.fullstackjavascript.cn:3000/hash3.html#${data}`;
</script>

// hash3.html
<script>
  let hash = location.hash;
  window.parent.parent.location.hash = hash 
</script>
複製程式碼

效果是沒問題的,不過感覺有點麻煩~

六.window.name

使用window.name跨域是利用切換路徑後window上的name屬性是會保留的。不懂?沒關係,舉個例子,還是有三個頁面a,b,c。a和b是同域下的,c自己一個域。a先引用c,c將想表達的內容放到name,屬性上之後,a改變引用路徑,改成引用b,此時name屬性不會被刪除,因為a,b是同域的,所以可以直接獲取。

// a.html
<iframe src="http://a.fullstackjavascript.cn:4000/c.html" id="myFrame" onload="load()" frameborder="0"></iframe>
<script>
  let first = true
  function load() {
    if(first){
      myFrame.src = 'http://b.fullstackjavascript.cn:3000/c.html';
      first = false
    }else{
      let name = myFrame.contentWindow.name;
      console.log(name);
    }
  }
</script>
// c.html
<script>
  window.name = '我愛你'
</script>
複製程式碼

直接訪問http://b.fullstackjavascript.cn:3000/a.html發現可以拿到我們想要的結果啦~

七.WebSocket

WebSocket最大特點就是,伺服器可以主動向客戶端推送資訊,客戶端也可以主動向伺服器傳送資訊,是真正的雙向平等對話。這個不是我們們考慮的問題。我們們現在的話題是跨域,恰恰WebSocket是沒有同源限制的!

可以直接建立socket物件和伺服器通訊發訊息

// socket.html
<script>
  let socket = new WebSocket('ws://localhost:4000');
  socket.onopen= function () {
    socket.send('我愛你');
  }
  socket.onmessage =  function (e) {
    console.log(e.data);
  }
</script>
複製程式碼

服務端需要ws模組

$ npm install ws
複製程式碼
let express = require('express');
let app = express();
let WebSockect = require('ws');
let wss = new  WebSockect.Server({port:4000});
wss.on('connection',function (ws) {
  ws.on('message',function (data) {
    console.log(data);
    ws.send('我不愛你');
  });
})
複製程式碼

我們都知道好東西往往不相容,我們可以使用socket.io模組保證相容性問題。這裡我就不過多去介紹websocket的使用了

八.Proxy

我們來看看服務端是如何實現代理的,也就是我們常說的反向代理!大家可能都對webpack比較熟悉了,webpack-dev-server內建了http-proxy-middleware。那好吧我們們就用express配合這個中介軟體外掛來試試這東西怎麼玩。

// 3000伺服器
let express = require('express');
let app = express();
var proxy = require('http-proxy-middleware');
let proxyOptions = {
  target: 'http://localhost:4000',
  changeOrigin: true
};
app.use('/api', proxy(proxyOptions));
app.listen(3000);
複製程式碼
// 4000伺服器
let express = require('express');
let app = express();
app.get('/api', (req,res) => {
  res.end('ok');
})
app.listen(4000);
複製程式碼

當我們訪問http://localhost:3000/api路徑時請求會被轉發到4000伺服器上,將4000伺服器上的結果響應給客戶端

九.Nginx

我相信大家對Nginx都並不陌生,他可以實現代理我們的介面。這裡我就簡單的使用一下展示下效果! 這裡以windows系統為例:

先要安裝nginx 下載地址

更改conf/nginx.conf增加訪問代理

location /api/ {
  proxy_pass http://localhost:4000;
}
複製程式碼

雙擊nginx.exe執行,訪問localhost/api發現返回了4000伺服器上的結果。

廢了這麼多口水,終於把前端常用的跨域手段介紹了一遍!有什麼疑問歡迎聯絡我!

結語

喜歡的點個贊吧^_^!
支援我的可以給我打賞哈!

打賞

相關文章