至簡·跨域 JSONP與CORS

什麼是跨域

什麼是跨域呢,對於從未聽過的人來說,是一個聽起來挺唬人名字。其實也沒什麼,完全可以通過字面意思去揣摩,坦白講,就是跨越不同的區域,就這個意思,當然爲什麼要去不同的區域呢,因爲要拿哪裏的資源呀,嘿嘿嘿嘿…。
咳咳,正經點啊,用官話來說:跨域,什麼是跨域呢?
就是指一個域名下的網站去請求另一個域名下網站的資源。爲什麼或出現跨域這種事情呢。原因就在於。瀏覽器的同源策略,就是爲了保密隱私,爲了自身的安全。並且ajax本身禁止發送跨域請求
跨域分以下幾種情況:

  1. http://www.acb.com -------ajax----- >http://uu.abc.com 域名級別不同
  2. http://hr.abc.com -------ajax----- >http://oa.abc.com 域名不同
  3. http://localhost:3000 -------ajax----- > http://localhast:8080 端口不同
  4. http://baidu.com -----ajax-----> https://baidu.com 協議不同
    協議不同的本質還是端口不同,https是443端口,http是80端口
  5. http://localhost ------ajax----> http://127.0.0.1 這個是個規定,規定自己本機下的這種訪問方式爲跨域。
    以上這幾種情況的ajax請求均屬於跨域請求。

知道了什麼是跨域,那麼就該去解決這問題,不然,我們怎麼去拿到我們想要的數據呢。提供兩種解決方案:

1.JSONP實現跨域

   先說說什麼是JSONP,JSON with Padding,填充式JSON,它是將其他網站上的JSON資料,作爲參數填充,填充到哪裏嫩?,其實使用jsonp時,後端會接受到一個參數,這個參數其實就是前端定義好了的一個處理函數的函數名,這個處理函數,需要一些數據,它將這些數據呢以外部傳參的形式呈現出來,,而我們也說了,jsonp請求的時候會將這個函數名獲取到,我們在使用一些拼接手法,將參數和函數名結合起來形成一個完整函數名加參數的形式,然後返回給客戶端,客戶端就可以得到自己所需的參數,又因爲數據是以json的格式傳輸的,就形成了這種填充式json的感覺,這也就是jsonp名字的來由,不得不說,這個方法是真的秒啊。 最後在加上動態添加script標籤,數據到了前端後,就會自動調用這個函數。
   至於前端是怎麼請求的呢,當然是通過script標籤的src啦,爲什麼要用script標籤的src呢,你有沒有發現你在引用第三方插件時,不論是網上的url,還是本地的url,script它都能找到啊,有木有??簡直比隔壁老王翻牆的本事都厲害啊…有木有。爲什麼呢?難不成,他真的是跟隔壁老王學得,而且青出於藍勝於藍。 不僅如此哦,html標籤還有幾個也可能是跟老王學的,例如iframe、style、還有img標籤,它的src也可以,emmm…這幾個不學好。
   開個玩笑哈,爲什麼呢,這是瀏覽器給的特權,是瀏覽器動態的訪問外域文件的一種方式,結果被大家給利用了,真的是單純的孩子。利用script的src這一特點,便可以實現跨域訪問。當使用script標籤進行外域資源請求時,就會將後端定義好的代碼下載到本地執行。


**2018-11-22日 — 補充**

具體過程:

  • 前端通過script標籤發起資源請求,參數中攜帶定義好的函數名。
  • 後端會將獲取到的函數名以及所需的參數作爲回發數據,響應至客戶端,被客戶端下載,客戶端下載到代碼後,將得到的函數及參數添加到新的script標籤中,就相當於執行之前定義的函數,只是,函數調用以及傳參是由後端發過來的。

相信你已經大致瞭解了這個過程,興許你還能發現jsonp的一些缺陷,即它的請求方式類似於get請求,不能進行post請求,如果要想向服務端post數據,會報請求格式不正確錯誤,並且如果有,那麼這種方式的數據都在url中,如果post一堆數據,url也會顯得冗餘,拼一長串也不太現實。並且如果要保存登錄狀態,這種方式是無法攜帶認證信息,jsonp是繞過了ajax請求請求資源的機制,轉而使用src的特性獲取資源,後端不能獲取一致的session,無法進行登錄狀態的檢查,然後就是jsonp的安全性問題,它無法設置請求限制,要是服務器不值得信賴,可能會下載到惡意的執行代碼,例如重定向到非法網站盜取用戶信息,或者簡單點的拿到一個死循環代碼,那不就涼涼了。
所謂的padding其實是指返回來的函數及參數,在此作以修正。


都退後,我要開始裝B了, 等一等,我想多說幾種方案。論一論他們的優缺點。

  • 方案一:
    服務端:將要返回的數據,填充進一條字符串格式的js語句中,組成一條正確的可執行的js語句,在返回到客戶端。
    客戶端:添加script標籤,“<script src=“服務端地址”>”
    結果script標籤可以跨域訪問,請求到服務端返回的js語句,並立刻執行。

例如服務端,返回的數據可以這樣寫

    const http = require('http');
	const url = require('url');
	http.createServer(function(req,res){
       var params = url.parse(req.url,true);
       console.log(params);//在方案三時有用
       res.writeHead(200,{
          "Content-Type":"application/json;charset=utf-8"
       });
       res.write(`alert("疼")`);
       res.end();
    }).listen(8080,()=>{
    console.log("Jsonp server is running");
});

客戶端,我未設路由,所以直接寫服務端地址即可

<script src="http://127.0.0.1:8080"></script>

客戶端加載網頁時便會彈出警告框。這只是一個簡單的實現。我們並不滿足於此。

  • 方案二:
    服務端:返回一條自定義函數的調用語句,客戶端必須執行指定名稱的函數
    客戶端:提前定義一個與服務端返回的同名的函數,有一個參數可以接受服務端的數據。
    例如:
    服務端:
    var data = "Welcom";
    res.write(`fun("${data}")`);
    res.end();

客戶端:

<script>
  function fun(data){
        alert(data);
    }
</script>
<script type="text/javascript" src="http://127.0.0.1:8080?fun"></script>

函數定義要在返回數據的script標籤之前進行,這裏的結果也會彈出警示框,並且攜帶了數據。但是美中不足的是,函數名是寫死的,不可能說我們在請求數據前,先跟服務端商量好,函數名用那個,那不搞笑呢麼。

  • 方案三
    這裏我們可以從前端請求時預先在參數內設定一個callback參數,像這樣。
<script type="text/javascript" src="http://127.0.0.1:8080?callback=fun"></script>

爲什麼這樣,因爲我們在後端接收請求時,可以獲取到callback參數,通過對url部分轉對象後可以獲取

     var params = url.parse(req.url,true);
     console.log(params.query.callback);

這是獲取到的部分

   query: { callback: 'fun' },

以此便可以使後端不受前端的限制,前端可以隨意定義自身所需的處理函數,並且在客戶端通過js代碼動態的創建script標籤,併爲其src屬性設置好請求的url。服務端也不用在乎前端定義的是什麼函數名了。

操作如下:
服務端:

const http = require('http');
const url = require('url');
http.createServer(function(req,res){
    var params = url.parse(req.url,true);
    console.log(params);
    console.log(params.query.callback);
    res.writeHead(200,{
       "Content-Type":"application/json;charset=utf-8"
    })
    var data = "Welcom";
   
    if(params.query.callback){
        var str3 = `${params.query.callback}("${data}")`;//定義要送函數名和函數參數
        console.log(str3);
        res.end(str3);
    }else{
        res.end();
    }
   
    //var data = "Welcom";
    //res.write(`fun("${data}")`);
    //res.end();
}).listen(8080,()=>{
    console.log("Jsonp server is running");
});

客戶端:

<script src="js/jquery-1.11.3.js"></script>
<script>
	//用jq的方式創建標籤,原生js也可以
    $('#btn1').on('click',function(){
        $(`<script type='text/javascript'  src='http://127.0.0.1:8080?callback=fun'>`).appendTo('body');
    });
    
    function fun(data){
        alert(data);
        $('body>script:last-child').remove();//每次請求刪除上一次請求的script標籤,防止script標籤因爲請求次數不斷的添加。
    }
</script>
<script type="text/javascript" src="http://127.0.0.1:8080?callback=fun"></script>

好了,最後一種方案是相對於前兩種方案的優化,推薦使用

2. CORS,此方法需要服務端與客戶端的配合

CORS跨域資源共享
服務端的響應頭設置:
res.writeHead(200,{
“Content-Type”:“application/json;charset=utf-8”,
"Access-Control-Allow-Origin”:"*" | " 指定的來源域名"
})
這樣即可, 設置爲"*"的方式會不限制任何域名的訪問,當然也可以指定允許訪問的來源域名,這種方式雖然簡單,但並不安全。

最後說一下JQ中實現JSONP跨域的方式

$.ajax({
	//在1.x版本中
url:" ... ",
type: "get"|"post",
data:"變量=值&..."|{變量:值,...},
dataType:"json",//通過這句將其設爲jsonp,便會實現jsonp跨域
success:function(res){ //在響應成功結束後自動
     //res就是響應的結果
 	  },
error(){ 當請求出錯時,自動觸發 },
complete(){ 無論成功還是出錯,只要請求結束就執行 }
	 })
//在 3.x版本中,實現了promise
.then(function(res){
 	 ...
 })

使用JQ進行jsonp跨域請求,不需要我們設置函數名,jq會隨機生成函數名,也會自動清除每次請求產生的script標籤,極大的放便了操作。其原理與方案三是一致的。

OK,完結,撒花


並未結束 **2018/11/22日補充**

之前的跨域設置針對簡單跨域,例如只設置了

Access-Control-Allow-Origin: ”*“

的跨域,先說說簡單跨域和複雜跨域:
簡單跨域,即get、post、head請求中的一種,並且頭信息也有幾項限制,最突出的一點就是Content-Type只能爲:application/x-www-form-urlencoded,multipart/form-data和text/plain中的一種
複雜跨域:不是以上兩種情況的,
對於複雜跨域,還需要設置一些相關信息。若要進行服務端驗證或者攜帶cookie還需要在客戶端和服務端配合設置。

由於跨域請求默認不發送Cookie和HTTP認證信息。相信應該明白,同源策略的目的,就是爲了保密,爲了防止數據被盜取,cookie中攜帶着用戶很多的信息,所以這好似服務端與客戶端兩個人之間的小祕密,不能被別人知道了,萬一被截取就可能發生跨域僞造請求的問題。但是實際生活中,我們經常會進行在線驗證等等,不能所有情況下都要重新登錄,這時就要保證服務端的完全信任了,然後攜帶cookie,如果非要把Cookie發到服務器,一方面要服務器同意,指定Access-Control-Allow-Credentials字段:Access-Control-Allow-Credentials: true,另一方面,開發者在發起ajax請求時設置withCredentials爲true。

   在這裏,當我們的服務器設置Access-Control-Allow-Credentials: true時,會產生新的問題,在瀏覽器標準中,當服務器中設置Access-Control-Allow-Credentials爲true時,Access-Control-Allow-Origin不能設置爲*,而Access-Control-Allow-Origin: *是我們常用的解決跨域問題的設置。

此問題的解決方案有兩種,第一種方案是簡單的設置一個白名單;另一種方案,如果之前設置Access-Control-Allow-Origin: *,此時可以在服務器配置文件進行設置:先獲取發起跨域請求的源域,然後設置Access-Control-Allow-Origin的值爲獲取到的源域。當然這個設置可能在後端某些配置文件裏,也可能直接在服務器配置文件設置。但思路一致。

具體實現:
客戶端說一下以下幾種條件下的使用:
默認跨域不攜帶cookie,所以需要聲明允許攜帶cookie。
(1) 原生js中設置

xhr.withCredentials=true;

(2)jq方式的在配置參數中設置

 xhrFields:{
            withCredentials:true
        },

(3)在axios中的設置

import axios from 'axios';
//設置允許攜帶cookie
axios.defaults.withCredentials=true;

服務端的配置,僅討論nodejs環境的配置方式,其他服務器的配置信息也一致
(1)設置響應頭,允許跨域攜帶認證信息:
我這裏使用express的框架
res.header(“Access-Control-Allow-Credentials”, true);

(2)在允許跨域攜帶認證信息後,
Access-Control-Allow-Origin",不能爲“*”,需要設置允許的源域名,或者可以自己設置一個白名單,允許這些可以跨域訪問。
res.header(“Access-Control-Allow-Origin”, “http://localhost:8080”);
這樣就可以實現,攜帶認證信息的進行訪問。

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