ajax跨域簡單請求與複雜請求
開發網站時經常會用到跨域資源共享(簡稱cors,後面使用簡稱)來解決跨域問題,但是在使用cors的時候,http請求會被劃分為兩類,簡單請求和複雜請求,而這兩種請求的區別主要在於是否會觸發cors預檢請求。
首先我們要明白cors的原理(引自MDN):
跨域資源共享標準新增了一組 HTTP 首部欄位,允許伺服器宣告哪些源站通過瀏覽器有許可權訪問哪些資源。另外,規範要求,對那些可能對伺服器資料產生副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 請求,或者搭配某些 MIME 型別的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否允許該跨域請求。伺服器確認允許之後,才發起實際的 HTTP 請求。在預檢請求的返回中,伺服器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關資料)
從上面的文字中我們得到如下資訊:
1、跨域資源共享標準新增了一組 HTTP 首部欄位,伺服器通過這些欄位來控制瀏覽器有權訪問哪些資源。
2、為了安全起見請求方式分為兩類,一類不會預先發送options請求,一些會預先發送options請求。
3、 GET 以外的 HTTP 請求,或者搭配某些 MIME 型別的 POST 請求會觸發options請求。
4、伺服器驗證OPTIONS完成後才會允許傳送世界的http請求。
不會觸發http預檢請求的便是簡單請求,想法能夠觸發http預檢請求的便是複雜請求。
那麼有哪些簡單請求呢?以下是來自MDN官方引用:
1、使用下列方法之一:
GET、
POST、
HEAD。
2、不得人為設定該集合之外的其他首部欄位。該集合為:
Accept
Accept-Language
Content-Language
Content-Type
3、Content-Type 的值僅限於下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
4、請求中的任意XMLHttpRequestUpload 物件均沒有註冊任何事件監聽器;XMLHttpRequestUpload 物件可以使用 XMLHttpRequest.upload 屬性訪問
5、請求中沒有使用 ReadableStream 物件
那什麼是複雜請求呢,除了簡單請求都是複雜請求。
簡單請求的傳送從程式碼上來看和普通的XHR沒太大區別,但是HTTP頭當中要求總是包含一個域(Origin)的資訊。該域包含協議名、地址以及一個可選的埠。不過這一項實際上由瀏覽器代為傳送,並不是開發者程式碼可以觸及到的。
簡單請求的部分響應頭及解釋如下:
Access-Control-Allow-Origin(必含)- 不可省略,否則請求按失敗處理。該項控制資料的可見範圍,如果希望資料對任何人都可見,可以填寫"*"。 Access-Control-Allow-Credentials(可選) – 該項標誌著請求當中是否包含cookies資訊,只有一個可選值:true(必為小寫)。如果不包含cookies,請略去該項,而不是填寫false。這一項與XmlHttpRequest2物件當中的withCredentials屬性應保持一致,即withCredentials為true時該項也為true;withCredentials為false時,省略該項不寫。反之則導致請求失敗。 Access-Control-Expose-Headers(可選) – 該項確定XmlHttpRequest2物件當中getResponseHeader()方法所能獲得的額外資訊。通常情況下,getResponseHeader()方法只能獲得如下的資訊: Cache-Control Content-Language Content-Type Expires Last-Modified Pragma 當你需要訪問額外的資訊時,就需要在這一項當中填寫並以逗號進行分隔
如果僅僅是簡單請求,那麼即便不用CORS也沒有什麼大不了,但CORS的複雜請求就令CORS顯得更加有用了。簡單來說,任何不滿足上述簡單請求要求的請求,都屬於複雜請求。比如說你需要傳送PUT、DELETE等HTTP動作,或者傳送Content-Type: application/json的內容。
複雜請求表面上看起來和簡單請求使用上差不多,但實際上瀏覽器傳送了不止一個請求。其中最先發送的是一種"預請求",此時作為服務端,也需要返回"預迴應"作為響應。預請求實際上是對服務端的一種許可權請求,只有當預請求成功返回,實際請求才開始執行。
預請求以OPTIONS形式傳送,當中同樣包含域,並且還包含了兩項CORS特有的內容
Access-Control-Request-Method – 該項內容是實際請求的種類,可以是GET、POST之類的簡單請求,也可以是PUT、DELETE等等。 Access-Control-Request-Headers – 該項是一個以逗號分隔的列表,當中是複雜請求所使用的頭部。
顯而易見,這個預請求實際上就是在為之後的實際請求傳送一個許可權請求,在預迴應返回的內容當中,服務端應當對這兩項進行回覆,以讓瀏覽器確定請求是否能夠成功完成。
複雜請求的部分響應頭及解釋如下:
Access-Control-Allow-Origin(必含) – 和簡單請求一樣的,必須包含一個域。 Access-Control-Allow-Methods(必含) – 這是對預請求當中Access-Control-Request-Method的回覆,這一回復將是一個以逗號分隔的列表。儘管客戶端或許只請求某一方法,但服務端仍然可以返回所有允許的方法,以便客戶端將其快取。 Access-Control-Allow-Headers(當預請求中包含Access-Control-Request-Headers時必須包含) – 這是對預請求當中Access-Control-Request-Headers的回覆,和上面一樣是以逗號分隔的列表,可以返回所有支援的頭部。這裡在實際使用中有遇到,所有支援的頭部一時可能不能完全寫出來,而又不想在這一層做過多的判斷,沒關係,事實上通過request的header可以直接取到Access-Control-Request-Headers,直接把對應的value設定到Access-Control-Allow-Headers即可。 Access-Control-Allow-Credentials(可選) – 和簡單請求當中作用相同 Access-Control-Max-Age(可選) – 以秒為單位的快取時間。預請求的的傳送並非免費午餐,允許時應當儘可能快取。
理論聊完之後,咱們來看一下實踐,首先啟動兩個服務,一個埠為3000,的靜態資源伺服器,用於請求介面,另一臺埠為5000的介面伺服器,如圖所示:
後端介面伺服器程式碼如下:
const express = require("express");
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended:false}));
app.use(bodyParser.json());
// 實現CORS
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'OPTIONS,GET,POST,PUT,DELETE');
res.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization");
res.header("cache-control", "no-cache");
res.header("content-type", "application/json; charset=utf-8");
res.header("ETag", '');
next();
});
app.post("/p",(req,res)=>{
res.send(req.body)
})
app.listen(5000,()=>{
console.log("5000")
})
前端請求資源指令碼程式碼如下:
axios.post("http://localhost:5000/p",{name:"zs",age:"18"}).then((data)=>{
console.log(data.data);
})
我們用axios這個http請求庫傳送了一個post請求,axios傳送post請求預設會把資料轉化為json格式,並且會預設設定請求頭:Content-Type:application/json,很顯然這是一個複雜請求,這樣的話,會觸發options請求。
我們分別啟動兩個服務,並開啟瀏覽器,訪問頁面,載入請求介面指令碼,觀察network如圖:
我們看到,程式碼中命名只發送了一次非同步請求為什麼顯示兩次呢?詳細截圖如下:
我們看到確實傳送了兩次請求一次為OPTIONS一次為POST,而我們程式碼中並沒有處理對OPTIONS請求的響應處理,所以上面服務端程式碼是不合理的,綜合考慮,OPTIONS請求並會對實際http請求差生影響,所以我們統一的對OPTIONS請求返回204,服務端負責支援CORS的中介軟體修正程式碼如下:
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'OPTIONS,GET,POST,PUT,DELETE');
res.header("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization");
res.header("cache-control", "no-cache");
res.header("content-type", "application/json; charset=utf-8");
res.header("ETag", '');
//header頭資訊設定結束後,結束程式往下執行,返回
if(req.method.toLocaleLowerCase() === 'options'){
res.status(204);
return res.json({}); //直接返回空資料,結束此次請求
}else{
next();
}
});
總結一下:
1. 簡單請求:
滿足一下兩個條件的請求。
(1) 請求方法是以下三種方法之一:
- HEAD
- GET
- POST
(2)HTTP的頭資訊不超出以下幾種欄位:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
2. 複雜請求:
非簡單請求就是複雜請求。
非簡單請求是那種對伺服器有特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type欄位的型別是application/json。
非簡單請求的CORS請求,會在正式通訊之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。
預檢請求為OPTIONS請求,用於向伺服器請求許可權資訊的。
預檢請求被成功響應後,才會發出真實請求,攜帶真實資料。
axios預設請求就是application/json,所以不需要自己加上頭部(不需要在config中加headers),所以總是會發出options請求的,看看是不是配置的時候加了不必要的headers配置項。
另外,如果真的需要預檢,後臺也需要進行設定,允許options請求。
&n