說說CORS與jsonp

wangxiaoxaio發表於2019-04-16

前言

瀏覽器出於防止潛在安全風險的考慮,使用了同源策略,這一方面保證了我們資料的安全, 另一方面卻又限制了我們的手腳,基於此,開發者們與標準制定組織提供了不同的解決方案, 這裡主要說說CORS與jsonp。

什麼是同源策略

同源指的是兩個域需要協議,子域名,主域名與埠號都保持一致,四者有一個不同,即屬於跨域。 注意: http://localhost:8080與http://127.0.0.1:8080不屬於同源,也就是說,即使IP地址一致,但是一個是域名,一個是IP地址,也不屬於同源。

CORS的使用與注意點

CORS:跨域資源共享,是W3C制定的一個草案,定義了在必須訪問跨源資源時,瀏覽器和伺服器該怎麼溝通。

CORS的實現原理是,瀏覽器發出請求報文中會額外包含一個Origin頭部,該頭部的值是當前頁面的源資訊(協議,域名和埠),伺服器收到請求報文後,如果同意這個跨源請求,就在響應報文的頭部新增Access-Control-Allow-Origin,值與請求報文中的Origin頭部的值一致,如果響應報文中沒有這個頭部或者有,但是值不一致,這次的跨源請求就會失敗。

瀏覽器原生支援CORS,但是不同的瀏覽器支援的方式不同。 IE8及以上版本引用了XDR(XDomainRequest)型別,這個物件和XHR物件類似,使用這個物件,可以實現安全可靠的跨域通訊。 XDR的使用與XHR類似,使用過程如下:

  1. 例項化XDR
var xdr = new XDomainRequest();
複製程式碼
  1. 呼叫open(),open()接收兩個引數,請求所用的方法以及URL,所有的XDR都是非同步執行的,所有不需要第三個引數。
   xdr.open('get','#');
複製程式碼
  1. 呼叫send(),如果使用的是get方法,傳入null,如果是post方法,傳入字串。
 xdr.send(null);
複製程式碼
  1. 為xdr繫結事件處理函式。xdr支援的事件包括load,error,timeout。需要說明一點的是,這部分的事件監聽器需要在呼叫open()之前宣告,這裡只是行文需要。 4.1 load事件 在接收到響應後,你可以訪問響應的原始文字,但是沒有辦法確定響應的狀態碼,只有在響應有效的/情況下才會觸發load事件,如果接收到的響應中不包含Access-Control-Allow-Origin頭部的話,則會觸發error事件。
xdr.onload=function(){
  console.log(xdr.resonseText);
}
複製程式碼

4.2 error事件 導致XDR請求失敗的因素很多,所有為每個xdr物件繫結該事件的處理函式是很有必要的,但是該事件丟擲的資訊有限,我們只能確定請求失敗了,並不能得知請求失敗的原因。

xdr.onerror=function(){
  console.log('get a error');
}
複製程式碼

4.3 timeout事件 xdr物件有一個timeout屬性,該屬性表明請求自發出多久後超時,當為其賦值後,在給定的時間內還沒接受到響應,就會觸發timeout事件。

xdr.ontimeout=function(){
  alert('time is too long');
}
複製程式碼

使用XDR需要注意的點:

  1. cookie不會隨請求傳送,也不會隨請求返回。
  2. 不能訪問響應頭部資訊,這意味著xdr物件沒有getResponseHeader()和getAllResponseHeaders()。
  3. 只支援get和post方法。
  4. 當使用post方法時,xdr物件有一個contentType屬性,這個屬性可以表示要傳送的資料的格式,這是xdr物件能夠影響頭部資訊的唯一方法。

其他瀏覽器對CORS的實現 其他主流瀏覽器通過XHR物件原生支援CORS,在嘗試開啟不同來源的資源時,XML可以自動觸發這個行為,有一點不同的是,open方法中的URL引數需要傳入絕對路徑。

使用XHR物件進行跨域通訊的注意點:

  1. 不能使用setRequestHeader()設定自定義頭部。
  2. 不能傳送和接收cookie。
  3. 呼叫getAllResponseHeaders()返回空字串,呼叫getResponseHeader()報錯。
  4. 由於同源請求和跨源請求都使用同樣的介面,因此對於本地資源,使用相對URL,對跨源的資源,使用絕對路徑,這樣可以避免跨源請求時的限制(見1,2)。

這個給出一個例子,本地檔案是index.html,服務端檔案為server.js,需要注意的是,我們需要使用http-server(其他的也可以)搭建一個靜態資源伺服器,關於http-server的使用,點選這裡,這樣可以使用http協議訪問index.html檔案,使用file協議無法使用CORS。

// index.html
<script>
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange=(req,res)=>{
            if(xhr.readyState==4){
                if((xhr.status>=200&& xhr.status<300)||xhr.status==304) {
                    console.log(xhr.responseText);
                    console.log(xhr.getResponseHeader('Connection'));
                } else {
                    console.log('Request was unsuccessful: '+xhr.statusText);
                }
            }
        };
        xhr.open('get','http://127.0.0.1:3000/',true);
        xhr.send(null);
</script>
複製程式碼
//server.js
const express = require("express");  //記得安裝express

const app = express();

app.get("/", function (req, res) {
    res.setHeader('Access-Control-Allow-Origin','http://localhost:8080');
    res.end('hello world!');
});

app.listen(3000, function () {
    console.log("app is listening 3000");
});
複製程式碼

CORS的高階使用技巧

CORS支援在跨域請求的過程中,使用自定義的頭部資訊,post和GET之外的方法,不同型別的主體內容和提高憑據(cookie)。在使用這些高階選項傳送請求時,瀏覽器會首先傳送一個Prefight請求,這種請求使用options方法,這是一種透明的伺服器驗證機制,開發者不需要做這些。 該請求這需要包含以下的頭部:

  1. Origin:與簡單的請求相同。
  2. Access-Control-Allow-Method:請求自身使用的方法(是指我們主動發起的跨域請求的方法,不是options)。
  3. Access-Control-Request-Headers:(可選)自定義的頭部資訊,多個頭部已逗號隔開。 除了這些頭部資訊外,xhr有withCredentials屬性,將該屬性設定未true,可以在請求過程中攜帶cookie。

伺服器在接收到這個請求後,如果同意這次跨域請求,就會在響應中設定響應的頭部資訊。 這些頭部資訊包括:

  1. Access-Control-Allow-Origin:與簡單的請求相同。
  2. Access-Control-Allow-Methods:允許的方法,多個方法以逗號分隔。
  3. Access-Control-Allow-Headers:允許的頭部,多個頭部以逗號隔開。
  4. Access-Control-Max-Age:應該將這個Preflight請求快取多長事件(以秒錶示)。 5.Access-Control-Allow-Credentials:布林值。

這裡給一個完整的例子:

//index.html
<script>
      var xhr = new XMLHttpRequest();
      document.cookie = "name=wang";  //BOM提供的介面,用於設定當前頁面所在的域的cookie
      xhr.withCredentials = true;  //允許在這次請求中攜帶cookie
      xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
          if ((xhr.status >= 200) & (xhr.status < 300) || xhr.status == 304) {
            console.log(xhr.responseText);
            console.log('name:'+xhr.getResponseHeader("name"));
          } else {
            console.log("Request was unsuccessful: " + xhr.status);
          }
        }
      };
      xhr.open("PUT", "http://localhost:4000/getData", true);  //使用put方法
      xhr.setRequestHeader("age", 12);  
      xhr.send(null);
    </script>
複製程式碼
//server1.js
//將該檔案與index.html放在一個目錄下
const Koa = require('koa');
const app = new Koa();
const server = require('koa-static');
const home = server(__dirname);
app.use(home);
app.listen(3000,()=>{
    console.log('app is runnint at port 3000');
})
//執行該程式碼後,可以在http://localhost:3000/index.html訪問到index.html。
複製程式碼
//server2.js
const Koa = require('koa');
const router = require('koa-router')();
const app = new Koa();

app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method}: ${ctx.request.url}`)
   await next();
})


app.use(async (ctx, next) => {
    ctx.response.set('Access-Control-Allow-Origin', 'http://localhost:3000');
    ctx.response.set('Access-Control-Allow-Headers', "age");
    ctx.response.set('Access-Control-Allow-Methods', "PUT");
    ctx.response.set('Access-Control-Allow-Credentials', true);
    ctx.response.set('Access-Control-Allow-Max-Age', 6);
    ctx.response.set('Access-Control-Expose-Headers', 'name');
    if (ctx.method === 'OPTIONS') {
        ctx.body = 'OPTIONS';
    };
    await next();
})

router.put('/getDate',async (ctx,next)=>{
    console.log(ctx.request.header.age);
    ctx.response.set('name','wang');
    ctx.body='put';
    await next();
})
app.use(router.routes());

app.listen(5000, () => {
    console.log('app is listening at port 5000');
});
複製程式碼

執行以上server1.js,server2.js檔案,訪問http://localhost:3000/index.html。

瀏覽器.png

伺服器.png
從以上的圖片中,可以看到,在進行跨域請求時,攜帶了cookie,並且瀏覽器在後臺替我們傳送了一個options方法的請求。 **總結:**CORS的高階用法其實就是在真正與伺服器進行跨域通訊時,瀏覽器會先傳送一個options方法的請求,幫我們跟伺服器進行‘溝通’,基於溝通結果,決定我們真正需要的跨域通訊的成功或失敗。

###jsonp

jsonp利用script標籤可以不受限制的從其他域載入資源的能力,進行跨域通訊。 jsonp由兩部分組成:回撥函式資料。回撥函式是響應帶來時,應該呼叫的函式,它需要在URL中指定;資料就是伺服器返回給瀏覽器的響應。

jsonp的使用

  1. 建立一個script元素。
  2. 宣告一個回撥函式。
  3. 為script指定src屬性的值,需要將回撥函式作為URL的查詢字串,形式為:'callback=functionName' 這裡給出一個完整的例子
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
    <script>
        var script = document.createElement('script');
      //回撥函式
        var blog = function(str){
          console.log(str);
        }
        script.src='http://localhost:3000/?callback=blog';
        document.body.appendChild(script);
        </script>
</body>
</html>
複製程式碼
//server.js
const Koa = require('koa');
const app = new Koa();
const router = require('koa-router')();

router.get('/',async(ctx,next)=>{
    var name = ctx.request.querystring.split('=')[1];
    console.log(name);
    var value='hello world!';
    ctx.body = `${name}('${value}')`;
})

app.use(router.routes());

app.listen(3000,()=>{
    console.log('app is running at port 3000');
})
複製程式碼

其實jsonp的內在邏輯很簡單,在script標籤中宣告的函式是屬於全域性的,當伺服器返回字串後,這個字串會被當做JavaScript程式碼執行,也就是呼叫之前宣告的函式。 ##總結 CORS屬於瀏覽器原生支援,支援所有型別的HTTP請求,是跨域通訊的根本解決方案。 jsonp是開發者們為了繞開同源策略的權宜之計,雖然只支援get方法,但是使用簡單。

總結

CORS屬於瀏覽器原生支援,支援所有型別的HTTP請求,是跨域通訊的根本解決方案。 jsonp是開發者們為了繞開同源策略的權宜之計,雖然只支援get方法,但是使用簡單。

參考

相關文章