跨域請求的三種處理方式JSONP,代理,CROS
跨域請求
場景:跨域請求報錯:
Failed to load http://localhost:3000/crossdomain/cors: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
- jsonp
- cors cross origin resource sharing
- 伺服器代理
1.JSONP:
只要說到跨域,就必須聊到 JSONP,JSONP 全稱為:JSON with padding,可用於解決老版本瀏覽器的跨域資料訪問問題。
由於 web 頁面上呼叫 js 檔案不受瀏覽器同源策略的影響,所以通過 script 標籤可以進行跨域請求:
- 首先前端需要先設定好回撥函式,並將其作為 url 的引數。
- 服務端接收到請求後,通過該引數獲取到回撥函式名,並將資料放在引數中將其返回
- 收到結果後因為是 script 標籤,所以瀏覽器會當做是指令碼進行執行,從而達到跨域獲取資料的目的
jsonp 之所以能夠跨域的關鍵在於頁面呼叫 JS 指令碼是不受同源策略的影響,相當於向後端發起一條 http 請求,跟後端約定好函式名,後端拿到函式名,動態計算出返回結果並返回給前端執行 JS 指令碼,相當於是一種 “動態 JS 指令碼”
接下來我們通過一個例項來嘗試:
後端邏輯:
// jsonp/server.js
const url = require("url");
require("http")
.createServer((req, res) => {
const data = {
x: 10
};
// 拿到回撥函式名
const callback = url.parse(req.url, true).query.callback;
console.log(callback);
res.writeHead(200);
res.end(`${callback}(${JSON.stringify (data)})`);
})
.listen(3000, "127.0.0.1");
console.log("啟動服務,監聽 127.0.0.1:3000");
前端邏輯:
// jsonp/index.html
<script>
function jsonpCallback(data) {
alert('獲得 X 資料:' + data.x);
}
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
然後在終端開啟服務:
之所以能用指令碼指令,是因為我在 package.json 裡面設定好了指令碼命令:
{
// 輸入 yarn jsonp 等於 "node ./jsonp/server.js & http-server ./jsonp"
"scripts": {
"jsonp": "node ./jsonp/server.js & http-server ./jsonp",
"cors": "node ./cors/server.js & http-server ./cors",
"proxy": "node ./serverProxy/server.js",
"hash": "http-server ./hash/client/ -p 8080 & http-server ./hash/server/ -p 8081",
"name": "http-server ./name/client/ -p 8080 & http-server ./name/server/ -p 8081",
"postMessage": "http-server ./postMessage/client/ -p 8080 & http-server ./postMessage/server/ -p 8081",
"domain": "http-server ./domain/client/ -p 8080 & http-server ./domain/server/ -p 8081"
},
// ...
}
yarn jsonp
// 因為埠 3000 和 8080 分別屬於不同域名下
// 在 localhost:3000 檢視效果,即可收到後臺返回的資料 10
至此,通過 JSONP 跨域獲取資料已經成功了,但是通過這種方式也存在著一定的優缺點:
優點:
- 它不像 XMLHttpRequest 物件實現 Ajax 請求那樣受到同源策略的限
- 相容性很好,在古老的瀏覽器也能很好的執行
- 不需要 XMLHttpRequest 或 ActiveX 的支援;並且在請求完畢後可以通過呼叫 callback 的方式回傳結果。
缺點:
- 它支援 GET 請求而不支援 POST 等其它類行的 HTTP 請求。
- 它只支援跨域 HTTP 請求這種情況,不能解決不同域的兩個頁面或 iframe 之間進行資料通訊的問題
- 無法捕獲 Jsonp 請求時的連線異常,只能通過超時進行處理
擴充套件:關於jsonp的誤區
-
動態請求就會有跨域的問題
跨域只存在於瀏覽器端,不存在於安卓/ios/Node.js/python/ java等其它環境
-
跨域就是請求發不出去了
跨域的請求是可以傳送出去的,並且服務端可以接收到請求並返回結果,只是結果被瀏覽器給攔截了。
2.CORS:
CORS 是一個 W3C 標準,全稱是"跨域資源共享"(Cross-origin resource sharing)它允許瀏覽器向跨源伺服器,發出 XMLHttpRequest 請求,從而克服了 ajax 只能同源使用的限制。
CORS 需要瀏覽器和伺服器同時支援才可以生效,對於開發者來說,CORS 通訊與同源的 ajax 通訊沒有差別,程式碼完全一樣。瀏覽器一旦發現 ajax 請求跨源,就會自動新增一些附加的頭資訊,有時還會多出一次附加的請求,但使用者不會有感覺。
因此,實現 CORS 通訊的關鍵是伺服器。只要伺服器實現了 CORS 介面,就可以跨域通訊。
前端邏輯很簡單,只要正常發起 ajax 請求即可:
// cors/index.html
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://127.0.0.1:3000', true);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
alert(xhr.responseText);
}
}
xhr.send(null);
</script>
這似乎跟一次正常的非同步 ajax 請求沒有什麼區別,關鍵是在服務端收到請求後的處理:
// cors/server.js
require("http")
.createServer((req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "http://localhost:8080",
"Content-Type": "text/html;charset=utf-8"
});
res.end("這是你要的資料:1111");
})
.listen(3000, "127.0.0.1");
console.log("啟動服務,監聽 127.0.0.1:3000");
成功的關鍵在於 Access-Control-Allow-Origin 是否包含請求頁面的域名,如果不包含的話,瀏覽器將認為這是一次失敗的非同步請求,將會呼叫 xhr.onerror 中的函式。
CORS 的優缺點:
- 使用簡單方便,更為安全
- 支援 POST 請求方式
- CORS 是一種新型的跨域問題的解決方案,存在相容問題,僅支援 IE 10 以上
擴充套件CORS:預檢請求
CORS把請求分為兩種,一種是簡單請求,另一種是複雜請求(需要觸發預檢請求),這兩者是相對的,怎樣才算“不簡單”?只要屬於下面的其中一種就不是簡單請求:
(1)使用了除GET/POST/HEAD之外的請求方式,如PUT/DELETE
(2)使用了除Content-Type/Accept等幾個常用的http頭
預檢請求使用OPTIONS方式去檢查當前請求是否安全,請求如下:
full | 204 | xhr |
---|---|---|
full | 200 | xhr |
服務端響應response headers如下:
Access-Control-Allow-Headers | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,body | 允許的請求頭 |
---|---|---|
Access-Control-Allow-Methods | GET, POST, OPTIONS, PUT, DELETE, PATCH | 允許的請求方式 |
Access-Control-Allow-Max-Age | 17280000 | 預檢請求有效期:傳送OPTIONS的時間間隔 |
如果在預檢請求檢測到當前請求不符合服務端設定的要求,則不會發出去了直接拋異常,這個時候就不用去發“複雜”的請求了。
為了支援CORS,nginx可以這麼配:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
}
3. 服務端代理:
伺服器代理,顧名思義,當你需要有跨域的請求操作時傳送請求給後端,讓後端幫你代為請求,然後最後將獲取的結果傳送給你。
程式碼如下:
// serverProxy/server.js
const url = require("url");
const http = require("http");
const https = require("https");
const server = http
.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
if (path === "topics") {
https.get("https://cnodejs.org/api/v1/topics", resp => {
let data = "";
resp.on("data", chunk => {
data += chunk;
});
resp.on("end", () => {
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8"
});
res.end(data);
});
});
}
})
.listen(3000, "127.0.0.1");
console.log("啟動服務,監聽 127.0.0.1:3000");
遺留思考問題:
- cookie token 具體的用處
- 網路安全
- 跨域網路攻擊。