Koa2框架利用CORS完成跨域ajax請求

實現跨域ajax請求的方式有不少,其中一個是利用CORS,而這個方法關鍵是在服務器端進行配置。ajax

本文僅對可以完成正常跨域ajax響應的,最基本的配置進行說明(深層次的配置我也不會)。json

CORS將請求分爲簡單請求和非簡單請求,能夠簡單的認爲,簡單請求就是沒有加上額外請求頭部的get和post請求,而且若是是post請求,請求格式不能是application/json(由於我對這一塊理解不深若是錯誤但願能有人指出錯誤並提出修改意見)。而其他的,put、post請求,Content-Type爲application/json的請求,以及帶有自定義的請求頭部的請求,就爲非簡單請求。跨域

簡單請求的配置十分簡單,若是隻是完成響應就達到目的的話,僅需配置響應頭部的Access-Control-Allow-Origin便可。數組

若是咱們在http://localhost:3000 域名下想要訪問 http://127.0.0.1:3001 域名。能夠作以下配置:服務器

app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
  await next();
});

而後用ajax發起一個簡單請求,例如post請求,就能夠輕鬆的獲得服務器正確響應了。
實驗代碼以下:cookie

$.ajax({
      type: 'post',
      url: 'http://127.0.0.1:3001/async-post'
    }).done(data => {
      console.log(data);
})

服務器端代碼:app

router.post('/async-post',async ctx => {
  ctx.body = {
    code: "1",
    msg: "succ"
  }
});

而後就能獲得正確的響應信息了。
這時候若是看一下請求和響應的頭部信息,會發現請求頭部多了個origin(還有一個referer爲發出請求的url地址),而響應頭部多了個Access-Control-Allow-Origin。async

如今能夠發送簡單請求了,可是要想發送非簡單請求仍是須要其餘的配置。post

當第一次發出非簡單請求的時候,實際上會發出兩個請求,第一次發出的是preflight request,這個請求的請求方法是OPTIONS,這個請求是否經過決定了這一個種類的非簡單請求是否能成功獲得響應。性能

爲了能在服務器匹配到這個OPTIONS類型的請求,所以須要本身作一箇中間件來進行匹配,並給出響應使得這個預檢可以經過。

app.use(async (ctx, next) => {
  if (ctx.method === 'OPTIONS') {
    ctx.body = '';
  }
  await next();
});

這樣OPTIONS請求就可以經過了。

若是檢查一下preflight request的請求頭部,會發現多了兩個請求頭。

Access-Control-Request-Method: PUT
Origin: http://localhost:3000

要經過這兩個頭部信息與服務器進行協商,看是否符合服務器應答條件。
很容易理解,既然請求頭多了兩個信息,響應頭天然也應該有兩個信息相對應,這兩個信息以下:

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: PUT,DELETE,POST,GET

第一條信息和origin相同所以經過。第二條信息對應Access-Controll-Request-Method,若是在請求的方式包含在服務器容許的響應方式之中,所以這條也經過。兩個約束條件都知足了,因此能夠成功的發起請求。

至此爲止,至關於僅僅完成了預檢,還沒發送真正的請求呢。
真正的請求固然也成功得到了響應,而且響應頭以下(省略不重要部分)

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: PUT,DELETE,POST,GET

請求頭以下:

Origin: http://localhost:3000

這就很顯而易見了,響應頭部信息是咱們在服務器設定的,所以是這樣。
而客戶端由於剛纔已經預檢過了,因此不須要再發Access-Control-Request-Method這個請求頭了。

這個例子的代碼以下:

$.ajax({
      type: 'put',
      url: 'http://127.0.0.1:3001/put'
    }).done(data => {
      console.log(data);
});

服務器代碼:

app.use(async (ctx, next) => {
   ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
   ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
   await next();
});

至此咱們完成了可以正確進行跨域ajax響應的基本配置,還有一些能夠進一步配置的東西。

好比,到目前爲止,每一次非簡單請求都會實際上發出兩次請求,一次預檢一次真正請求,這就比較損失性能了。爲了能不發預檢請求,能夠對以下響應頭進行配置。

Access-Control-Max-Age: 86400

這個響應頭的意義在於,設置一個相對時間,在該非簡單請求在服務器端經過檢驗的那一刻起,當流逝的時間的毫秒數不足Access-Control-Max-Age時,就不須要再進行預檢,能夠直接發送一次請求。

固然,簡單請求時沒有預檢的,所以這條代碼對簡單請求沒有意義。
目前代碼以下:

app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
  ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
  ctx.set('Access-Control-Max-Age', 3600 * 24);
  await next();
});

到如今爲止,能夠對跨域ajax請求進行響應了,可是該域下的cookie不會被攜帶在請求頭中。若是想要帶着cookie到服務器,而且容許服務器對cookie進一步設置,還須要進行進一步的配置。

爲了便於後續的檢測,咱們預先在http://127.0.0.1:3001這個域名下設置兩個cookie。注意不要錯誤把cookie設置成中文(剛纔我就設置成了中文,結果報錯,半天沒找到出錯緣由)

而後咱們要作兩步,第一步設置響應頭Access-Control-Allow-Credentials爲true,而後在客戶端設置xhr對象的withCredentials屬性爲true。

客戶端代碼以下:

$.ajax({
      type: 'put',
      url: 'http://127.0.0.1:3001/put',
      data: {
        name: '黃天浩',
        age: 20
      },
      xhrFields: {
        withCredentials: true
      }
    }).done(data => {
      console.log(data);
    });

服務端以下:

app.use(async (ctx, next) => {
   ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000');
   ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
   ctx.set('Access-Control-Allow-Credentials', true);
   await next();
});

這時就能夠帶着cookie到服務器了,而且服務器也能夠對cookie進行改動。可是cookie還是http://127.0.0.1:3001域名下的cookie,不管怎麼操做都在該域名下,沒法訪問其餘域名下的cookie。

如今爲止CORS的基本功能已經都提到過了。
一開始我不知道怎麼給Access-Control-Allow-Origin,後來經人提醒,發現能夠寫一個白名單數組,而後每次接到請求時判斷origin是否在白名單數組中,而後動態的設置Access-Control-Allow-Origin,代碼以下:

app.use(async (ctx, next) => {
  if (ctx.request.header.origin !== ctx.origin && whiteList.includes(ctx.request.header.origin)) {
    ctx.set('Access-Control-Allow-Origin', ctx.request.header.origin);
    ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
    ctx.set('Access-Control-Allow-Credentials', true);
    ctx.set('Access-Control-Max-Age', 3600 * 24);
  }
  await next();
});

這樣就能夠不用*通配符也可匹配多個origin了。
注意:ctx.origin與ctx.request.header.origin不一樣,ctx.origin是本服務器的域名,ctx.request.header.origin是發送請求的請求頭部的origin,兩者不要混淆。

最後,咱們再稍微調整一下自定義的中間件的結構,防止每次請求都返回Access-Control-Allow-Methods以及Access-Control-Max-Age,這兩個響應頭實際上是沒有必要每次都返回的,只是第一次有預檢的時候返回就能夠了。

調整後順序以下:

app.use(async (ctx, next) => {
  if (ctx.request.header.origin !== ctx.origin && whiteList.includes(ctx.request.header.origin)) {
    ctx.set('Access-Control-Allow-Origin', ctx.request.header.origin);
    ctx.set('Access-Control-Allow-Credentials', true);
  }
  await next();
});

app.use(async (ctx, next) => {
  if (ctx.method === 'OPTIONS') {
    ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET');
    ctx.set('Access-Control-Max-Age', 3600 * 24);
    ctx.body = '';
  }
  await next();
});

這樣就減小了多餘的響應頭。

相關文章
相關標籤/搜索