jsonp-反向代理-CORS解決JS跨域問題的我的總結(更新 v2.0)

網上說了不少不少,可是看完以後仍是很混亂,因此我本身從新總結一下。

解決 js 跨域問題一共有8種方法:javascript

  1. jsonp(只支持 get)
  2. 反向代理
  3. CORS
  4. document.domain + iframe 跨域
  5. window.name + iframe 跨域
  6. window.postMessage
  7. location.hash + iframe
  8. web sockets

各個方法都有各自的優缺點,可是目前前端開發方面比較經常使用的是 jsonp,反向代理,CORS:php

  • CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。html

    • 優勢是:正統,符合標準,
    • 缺點是:須要服務器端配合,比較麻煩。
  • JSONP的核心則是動態添加<script>標籤來調用服務器提供的js腳本。前端

    • 優勢是:對舊式瀏覽器支持較好,
    • 缺點1: 只支持 get 請求。
    • 缺點2:有安全問題(請求代碼中可能存在安全隱患)。
    • 缺點3:要肯定jsonp請求是否失敗並不容易。
  • 反向代理都可以兼容以上的肯定,可是僅僅做爲前端開發模式的時候使用,在正式上線環境較少用到。java

    • 通常開發環境的域名跟線上環境不同才須要這樣處理。
    • 若是線上環境太複雜,使用反向代理實現跨域的將會變得很麻煩,那麼這時候會須要採用 jsonp 或者 CORS 來處理。
這裏主要說明這三種方式。其餘方式暫不說明。

1、什麼是跨域問題

跨域問題通常只出如今前端開發中使用 javascript 進行網絡請求的時候,瀏覽器爲了安全訪問網絡請求的數據而進行的限制。node

提示的錯誤大體以下:jquery

No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://XXXXXX' is therefore not allowed access.

2、爲何會出現跨域問題

由於瀏覽器受到同源策略的限制,當前域名的js只能讀取同域下的窗口屬性。ios

換句話來講,就是跨越了瀏覽器的同源策略限制的時候,就會觸發了咱們所說的「跨域」問題。git

2.1 什麼是同源策略

同源指的是三個源頭同時相同:github

  • 協議相同
  • 域名相同
  • 端口相同

舉例來講,http://www.example.com/dir/page.html這個網址,

協議是 http://
域名是 www.example.com
端口是80 

//它的同源狀況以下:
http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不一樣源(域名不一樣)
http://v2.www.example.com/dir/other.html:不一樣源(域名不一樣)
http://www.example.com:81/dir/other.html:不一樣源(端口不一樣)

總的來講,只要不是三者同時相同,那麼就不是同源,那麼就會觸發同源策略限制。

2.2 同源策略限制了什麼

限制了:

  • Cookie、LocalStorage 和 IndexDB 沒法讀取
  • DOM 和 JS 對象沒法獲取
  • Ajax請求發送不出去

這就是咱們日常所說的「跨域問題」。

詳細的同源策略相關,能夠參考http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

3、解決跨域問題

3.1 使用反向代理方式

3.1.1 什麼是反向代理?

反向代理和正向代理的區別:

  • 正向代理(Forward Proxy),一般都被簡稱爲代理,就是在用戶沒法正常訪問外部資源,比方說受到GFW的影響沒法訪問twitter的時候,咱們能夠經過代理的方式,讓用戶繞過防火牆,從而鏈接到目標網絡或者服務。

  • 反向代理(Reverse Proxy)是指以代理服務器來接受 Internet 上的鏈接請求,而後將請求轉發給內部網絡上的服務器,並將從服務器上獲得的結果返回給 Internet 請求鏈接的客戶端,此時,代理服務器對外就表現爲一個服務器。

那麼能夠利用反向代理的原理,咱們經過一箇中間代理服務器(反向代理服務器),將客戶端網絡請求的一些 host,domain,port 和協議等東西進行改寫,使其模擬爲能夠訪問目標服務器的請求,模擬成不觸犯同源策略的請求去請求目標服務器。

3.1.2 如何使用反向代理服務器來解決跨域問題

  • 前端ajax請求的是本地反向代理服務器
  • 本地反向代理服務器接收到後:

    • 修改請求的 http-header 信息,例如 referer,host,端口等
    • 修改後將請求發送到實際的服務器
  • 實際的服務器會覺得是同源(參考同源策略)的請求而做出處理

如今前端開發通常使用 nodejs來作本地反向代理服務器

// 在 express 以後引入路由
var app = express();

var apiRoutes = express.Router();

app.use(bodyParser.urlencoded({extended:false}))

// 訪問反向代理的路由地址
apiRoutes.get("/lyric", function (req, res) {
  var url = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg";
  // 改寫源端的請求信息,而後重定向到目標服務器
  axios.get(url, {
    headers: { // 修改 header
      referer: "https://c.y.qq.com/",
      host: "c.y.qq.com"
    },
    params: req.query
  }).then((response) => {
    var ret = response.data
    if (typeof ret === "string") {
      var reg = /^\w+\(({[^()]+})\)$/;
      var matches = ret.match(reg);
      if (matches) {
        ret = JSON.parse(matches[1])
      }
    }
    res.json(ret)
  }).catch((e) => {
    console.log(e)
  })
});

// 使用這個路由
app.use("/api", apiRoutes);

這段代碼的執行原理是:

  1. node js 做爲反向代理服務器,而後在它上面使用express實現路由功能,
  2. 在 nodejs 裏面加入一條負責源端請求的路由映射,將它映射到目標服務器的 api 接口上,而且在這條路由裏面將實現請求的改寫,模擬目標服務器 api 接口的同源策略所需的要求。
  3. 源端會先請求 nodejs 反向代理服務器的以前設置的那條路由,會將參數傳給他,而後nodejs 反向代理會將它的請求進行改寫,而後轉發到目標服務器。

3.2 使用JSONP方式

3.2.1 什麼是 JSONP

JSONP有些文章會叫動態建立script,由於他確實是動態寫入 script 標籤的內容從而達到跨域的效果:

  • AJAX 沒法跨域是受到「同源政策」的限制,可是帶有src屬性的標籤(例如<script>、<img>、<iframe>)是不受該政策限制的,所以咱們能夠經過向頁面中動態添加<script>標籤來完成對跨域資源的訪問,這也是 JSONP 方案最核心的原理,換句話理解,就是利用了前端請求靜態資源的時候不存在跨域問題這個思路
  • JSONP 只能用 get 方式。
  • JSONP(JSON Padding)也叫填充式JSON,他是 json 的一種使用方式,它容許用戶傳遞一個callback參數給服務端,而後服務端返回數據時會將這個callback參數做爲函數名來包裹住JSON數據,這樣客戶端就能夠隨意定製本身的函數來自動處理返回數據了。

3.2.1 如何使用JSONP來解決跨域問題:

簡單一點的例子:

經過不受同源策略限制的標籤,例如 script,將一段js代碼間接地從外部引入。經過script標籤向目標源發起一個GET請求,服務器根據請求的參數返回包含js的代碼。

//本地代碼
<script>
    // 這個函數名字跟服務器返回的那段 js 的函數名字是同樣的,因此可以實現調用
    function getData(obj) { // 參數是一個對象
        var data = JSON.parse(obj);
        console.log(data.name);//jiavan
        console.log(data.age);//20
    }
</script>
<script src="http://cv.jiavan.com/test/data.php?callback=getData"></script>

//服務器上的代碼
<?php
    $func = $_GET['callback'];
    $data = '{"name": "jiavan", "age": 20}';
    echo $func."(".$data.");";
?>

// 服務器返回的數據是一段 js 代碼
getData( // 這是 js 的函數寫法
    {  // 這是參數,參數是一個對象
        "name":"jiavan",
        "age": 20
    }
)

先在本地定義了一個函數,這是用來處理來自服務器上數據的函數,下面用一個script標籤,而且向服務器發起了一個GET請求,而且指定了處理數據的回調函數,即上方的getData,服務器收到請求後返回了getData('{"name": "jiavan", "age": 20}');,即便一段js代碼,將數據傳入到回調函數中處理,這樣便完成了跨域。

參考:http://www.javashuo.com/article/p-fudpqfru-ev.html

複雜一點的例子:

引用來自http://www.javashuo.com/article/p-ayiskmas-bm.html的圖

  • 客戶端和服務器端約定一個參數名是表明 jsonp 請求的,例如約定 callback 這個參數名。
  • 而後服務器端準備好針對以前約定的 callback 參數請求的 javascript 文件,這個文件裏面要有一個函數名,要跟客戶端請求的時候的函數名要保持一致。(以下面例子:ip.js
  • 而後客戶端註冊一個本地運行的函數,而且函數的名字要跟去請求服務器進行 callback 回調的函數的名字要一致。(以下面例子:foo 函數跟請求時候callback=foo的名字是一致的)
  • 而後客戶端對服務器端進行 jsonp 的方式請求。
  • 服務器端返回剛纔配置好的js 文件(ip.js)到客戶端
  • 客戶端瀏覽器,解析script標籤,並執行返回的javascript文件,此時數據做爲參數,傳入到了客戶端預先定義好的 callback 函數裏。

    • 至關於本地執行註冊好foo 函數,而後獲取了一個foo 函數,而且這個獲取的 foo 函數裏面包含了傳入的參數(例如 foo({XXXXX})

服務器端文件ip.js

foo({
  "ip": "8.8.8.8"
});

客戶端文件 jsonp.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script>
        // 動態插入 script 標籤到 html 中
        function addScriptTag(src) {
          var script = document.createElement('script');
          script.setAttribute("type","text/javascript");
          script.src = src;
          document.body.appendChild(script);
        }
        // 獲取 jsonp 文件
        window.onload = function () {
          addScriptTag('http://example.com/ip?callback=foo');
        }
        // 執行本地的 js 邏輯,這個要跟獲取到的 jsonp 文件的函數要一致
        function foo(data) {
          console.log('Your public IP address is: ' + data.ip);
        };
    </script>
</head>
<body>
</body>
</html>

3.3 CORS 方式

CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。它容許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

  • CORS須要瀏覽器和服務器同時支持。目前,全部瀏覽器都支持該功能,IE瀏覽器不能低於IE10。
  • 整個CORS通訊過程,都是瀏覽器自動完成,不須要用戶參與。對於開發者來講,CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。
所以,實現CORS通訊的關鍵是服務器端。只要服務器端實現了CORS接口,就能夠跨源通訊。

3.3.1 CORS的請求分爲兩類

  • 簡單請求
  • 非簡單請求

只要同時知足如下兩大條件,就屬於簡單請求。

(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

凡是不一樣時知足上面兩個條件,就屬於非簡單請求。

3.3.2 對簡單請求處理

若是是簡單請求的話,會自動在頭信息之中,添加一個Origin字段,Origin字段用來講明,本次請求來自哪一個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否贊成此次請求。

GET /cors HTTP/1.1
Origin: http://api.bob.com 
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段,就知道出錯了,從而拋出一個錯誤,被XMLHttpRequestonerror回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於HTTP迴應的狀態碼有多是200。

這個Origin對應服務器端的Access-Control-Allow-Origin設置,因此通常來講須要在服務器端加上這個Access-Control-Allow-Origin 便可,相似這種:

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

3.3.3 非簡單請求

若是是非簡單請求的話,會在正式通訊以前,增長一次HTTP查詢請求,稱爲"預檢"請求(preflight)

瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。

須要注意這裏是會發送2次請求,第一次是預檢請求,第二次纔是真正的請求!

首先發出預檢請求:

// 預檢請求
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..

除了Origin字段,"預檢"請求的頭信息包括兩個特殊字段。

(1)Access-Control-Request-Method

該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。

(2)Access-Control-Request-Headers

該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,上例是X-Custom-Header。

而後服務器收到"預檢"請求之後:

檢查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段之後,確認容許跨源請求,就能夠作出迴應。

// 預檢請求的迴應
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

最後一旦服務器經過了"預檢"請求:

之後每次瀏覽器正常的CORS請求,就都跟簡單請求同樣,會有一個Origin頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin頭信息字段。

// 之後的請求,就像拿到了通行證以後,就不須要再作預檢請求了。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

詳情參考這裏http://www.ruanyifeng.com/blog/2016/04/cors.html

總的來講,只須要知道2個地方便可,其餘的能夠舉一反三:

  • CORS 須要服務器那邊加一個Access-Control-XXX的處理,目的是爲了處理請求的來源判別。
  • CORS 對於非簡單請求會增長一次 OPTIONS 的請求。

參考文檔:

相關文章
相關標籤/搜索