跨域不完全探究
前言
首先歡迎大家關注我的Github部落格,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。最近工作上事情比較繁多導致部落格一度斷更,為了挽救慘淡的關注量併兼顧自己有限的時間,準備近期多推出一些有關前端的基礎知識學習,一起夯實基礎。希望大家多多關注呀!
引子
跨域問題在Web開發中一直都是一個非常常見的問題,但在日常工作說實話使用頻率並沒有那麼多,再加上重使用輕原理導致我對跨域問題理解的一直非常的淺顯,藉此機會讓我們好好探索一下。
同源策略
跨域問題的起源於眾所周知的瀏覽器同源策略。作為如今Web安全基石的同源策略,早在上個世紀九十年代由網景公司提出,當時僅僅只是針對於Cookie
的訪問,即不同源的網頁之間Cookie
不能共享。後來同源策略變得更加嚴格,安全性也逐步提高,升級為:
- 不同源域 Cookie、LocalStorage 和 IndexDB 無法讀取。
- 不同源域 DOM 無法獲得。
- 不同源域 AJAX 請求不能傳送
說了這麼多同源,那麼何為同源。通常情況下,如果一個源擁有相同的協議(protocol)、主機(host)、埠(port),則稱其為同源。當然我們說了通常情況下,也就意味著這個方面有例外存在,IE毫無疑問的在這個方面承擔了它固有的職責。IE並沒有將埠號加入到同源策略的組成部分,因此http://host:81
和http://host:80
是屬於同源的。雖然同源的安全限制是必要的,但是同時也帶了束縛,假設我確實需要向不同源的地址傳送請求該怎麼辦呢?那就回到了我們所要討論的跨域問題。
跨域解決方案
JSONP
每每提起跨域問題,第一個想到的就是JSONP了。JSONP與JSON雖然看起來很像,但卻有本質區別
。JSONP全稱是JSON with Padding,並不是一種新的資料格式,而是資料格式JSON的一種“使用模式”。瀏覽器的同源限制對含有src
屬性的元素(例如: script
、img
)不起作用,JSONP就是利用了script
的這個特性。
基本原理
前面我們說了JSONP利用的是script
標籤,繞過瀏覽器的同源策略,試想我們通過一個URL獲取JSON資料是可能的。然而JSON資料卻是不可執行的,因此JSONP通過將返回的JSON資料用函式包裹來實現執行的目的,這也就是JSONP中P表示padding(包裹)的含義。因為需要函式包裹資料,所以前後端需要提前約定好函式名,我們一般會在請求的URL中帶上函式名稱作為引數。例如請求介面:
http://api.demo.com/data?userid=1&jsonp=parseResponse
複製程式碼
伺服器會將對應的資料填充到函式parseResponse
:
parseResponse({"Name": "小明", "Id" : 1823, "Rank": 7})
複製程式碼
概念實現
我們實現一個最簡單的函式用來建立一個JSONP請求:
function createJSONPRequest(url, callbackName){
var script = document.createElement("script");
script.src = url +"?callback=handleResponse";
document.body.appendChild(script);
}
function handleResponse(res){
console.log(res);
}
createJSONPRequest("http://localhost:3001/jsonp", "parseResponse")
複製程式碼
然後我們用express.js來響應這個JSONP請求:
// 伺服器埠號:3001
app.get('/jsonp', (req, res) => {
var callbackName = req.query.callback;
var mockData = {
name: "MrErHu"
};
res.send(`${callbackName}(${JSON.stringify(mockData)})`);
})
複製程式碼
執行時你會發現瀏覽器會正確列印出跨域訪問的資料,說明我們的跨域請求成功。
實踐
當然我們並不需要在前端手動去實現該函式,很多第三方庫已經封裝了JSONP的請求,我們以JQuery提供的Ajax理由為例,上面的請求用JQuery去實現程式碼是:
// 客戶端埠號:3000
$.ajax({
url: "http://localhost:3001/jsonp",
type: "GET",
dataType: "jsonp",
jsonpCallback: "parseResponse",
success: function (res) {
console.log(res);
}
});
複製程式碼
我們可以看到在JQuery中提供的ajax
函式中通過配置dataType
為jsonp
,則可以實現JSONP請求,需要注意的是JQuery只是將其封裝在ajax
函式內,二者實質上有本質區別。
瞭解JSONP的實現原理,我們知道這玩意肯定不會存在瀏覽器相容性問題,畢竟我不相信會存在不支援script
標籤的瀏覽器。但是因為正是通過script
標籤實現的,JSONP也就只能支援GET
請求,那麼如果我們想實現POST
請求的跨域有什麼好的辦法嗎?
CORS
CORS是Cross-origin resource sharing的縮寫,即跨域資源共享,屬於W3C標準,允許跨域傳送XMLHttpRequest
請求,支援多種HTTP Method。與JSONP的原理不同的是,CORS採用的是前後端HTTP表頭協商的方式(我自己起的名字)判斷是否允許跨域訪問,並且相比與JSONP來說,CORS需要客戶端和服務端共同支援,並且整個過程由瀏覽器自動完成。對於前端開發者而言,跨域的CORS通訊與普通的Ajax通訊沒有差別,整個跨域處理的過程主要集中在伺服器端。CORS通訊分成簡單請求(simple request)和非簡單請求(not-so-simple request)兩種模式。
簡單請求
所謂的簡單請求是指,請求方法屬於: HEAD
、GET
、POST
之一,並且HTTP的頭資訊不超出以下幾種欄位:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:僅限於三個值:application/x-www-form-urlencoded、multipart/form-data、text/plain
複製程式碼
如果不滿足以上任一條件,均屬於非簡單請求。對於簡單請求,瀏覽器會直接傳送CORS請求。當我們使用Ajax傳送一條跨域請求,瀏覽器會在HTTP請求頭(Request Headers)中增加Origin
屬性:
Origin
屬性用來表明當前的請求來自於哪個源(協議、主機、埠)。如果服務端支援CORS跨域,則需要在響應頭中返回以下屬性:
Access-Control-Allow-Origin:
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
複製程式碼
Access-Control-Allow-Origin
: 表示跨域的訪問的源,其中"*"表示允許來自任何源的請求跨域訪問Access-Control-Allow-Credentials
: 表示跨域訪問是否允許帶有Cookie資訊,該值僅可以允許設定為true
。如果伺服器不允許請求攜帶Cookie,則不需要傳送該屬性。值得注意的是,攜帶的Cookie資訊僅僅只是所跨域的伺服器域名設定的Cookie,不能攜帶其他域名下的Cookie資訊,Cookie仍然循序同源策略。並且還需要滿足Access-Control-Allow-Origin
指定的域與當前的請求的域完全一致(不能是"*")以及在XMLHttpRequest顯式設定withCredentials
屬性為true
,表示瀏覽器也允許傳送Cookie。Access-Control-Expose-Headers
: 該欄位可選,表示瀏覽器可以從該XMLHttpRequest
請求中拿到的響應頭資訊。預設僅能拿到Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
這六個屬性,如果想取得其他的屬性,必須在該屬性中指定。
我們用express.js
模擬一下:
// 伺服器埠號:3001
app.post('/cors', (req, res) => {
var mockData = {
name: "MrErHu"
};
var origin = req.get("origin");
res.set("Access-Control-Allow-Origin", origin);
res.set("Access-Control-Allow-Credentials", true);
res.send(JSON.stringify(mockData))
});
複製程式碼
當我們前端Ajax請求:
// 瀏覽器埠號:3000
$.ajax({
url: "http://localhost:3001/cors",
type: "POST",
success: function (res) {
console.log(res);
}
});
複製程式碼
你會發現該條請求已經不會出現跨域問題,可以正確訪問到資料。
非簡單請求
對於非簡單請求,瀏覽器會預先傳送一次預檢請求,詢問伺服器是否允許跨域訪問以及是否允許HTTP方法,只有得到肯定答覆後,瀏覽器才會發出正式的HTTP請求。比如我們跨域向伺服器傳送一次PUT
請求:
// 瀏覽器埠號:3000
$.ajax({
url: "http://localhost:3001/cors",
type: "PUT",
success: function (res) {
console.log(res);
}
});
複製程式碼
你會發現瀏覽器首先會傳送OPTIONS請求去預檢本次跨域請求。
當預檢請求檢測了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
屬性後就會正式傳送請求。需要注意的是,不僅僅是上述三個屬性,預檢請求還會返回Access-Control-Max-Age
屬性用來表示該條預檢請求的有效期,在有效期內,瀏覽器不會再次傳送預檢請求,僅會使用該條快取的預檢請求。
我們用express.js
來模擬本次請求:
// 伺服器埠號:3001
app.options('/cors', (req, res) => {
var origin = req.get("origin");
res.set("Access-Control-Allow-Origin", origin);
res.set("Access-Control-Allow-Credentials", true);
res.set("Access-Control-Allow-Methods", "PUT");
res.set("Access-Control-Max-Age", 24 * 60 * 60 * 1000);
res.send();
});
app.put('/cors', (req, res) => {
var mockData = {
name: "MrErHu"
};
var origin = req.get("origin");
res.set("Access-Control-Allow-Origin", origin);
res.set("Access-Control-Allow-Credentials", true);
res.send(JSON.stringify(mockData))
});
複製程式碼
你就會發現本次PUT
跨域請求成功,正確返回資料,達到了我們跨域的要求。
對比
與JSONP相比,CORS支援更多的HTTP方法並且整個過程由瀏覽器自動完成,但是僅僅只相容IE10及以上的瀏覽器,如果需要相容老版本IE瀏覽器,CORS可能就不適合你了。
後言
我們這邊文章著重講述了兩種跨域的基本原理,但實際上圍繞跨域問題還有很多值得學習的地方,比如後端對跨域請求的處理,以及前端跨域帶來種種的安全性問題。其實不僅僅是JSONP與CORS,Websocket也能實現跨域請求,以後有機會可以學習一下,最後還是歡迎大家關注我的部落格,水平欠佳,希望體諒,願一同進步。