1. 程式人生 > >跨域請求的三種處理方式JSONP,代理,CROS

跨域請求的三種處理方式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 標籤可以進行跨域請求:

  1. 首先前端需要先設定好回撥函式,並將其作為 url 的引數。
  2. 服務端接收到請求後,通過該引數獲取到回撥函式名,並將資料放在引數中將其返回
  3. 收到結果後因為是 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 跨域獲取資料已經成功了,但是通過這種方式也存在著一定的優缺點:

優點:

  1. 它不像 XMLHttpRequest 物件實現 Ajax 請求那樣受到同源策略的限
  2. 相容性很好,在古老的瀏覽器也能很好的執行
  3. 不需要 XMLHttpRequest 或 ActiveX 的支援;並且在請求完畢後可以通過呼叫 callback 的方式回傳結果。

缺點:

  1. 它支援 GET 請求而不支援 POST 等其它類行的 HTTP 請求。
  2. 它只支援跨域 HTTP 請求這種情況,不能解決不同域的兩個頁面或 iframe 之間進行資料通訊的問題
  3. 無法捕獲 Jsonp 請求時的連線異常,只能通過超時進行處理

擴充套件:關於jsonp的誤區

  1. 動態請求就會有跨域的問題

    跨域只存在於瀏覽器端,不存在於安卓/ios/Node.js/python/ java等其它環境

  2. 跨域就是請求發不出去了

    跨域的請求是可以傳送出去的,並且服務端可以接收到請求並返回結果,只是結果被瀏覽器給攔截了。

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 的優缺點:

  1. 使用簡單方便,更為安全
  2. 支援 POST 請求方式
  3. 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");

遺留思考問題:

  1. cookie token 具體的用處
  2. 網路安全
  3. 跨域網路攻擊。