1. 程式人生 > >瀏覽器跨域問題 瀏覽器的同源策略及跨域解決方案

瀏覽器跨域問題 瀏覽器的同源策略及跨域解決方案

瀏覽器的同源策略及跨域解決方案

 

同源策略

一個源的定義

如果兩個頁面的協議,埠(如果有指定)和域名都相同,則兩個頁面具有相同的

舉個例子:

下表給出了相對http://a.xyz.com/dir/page.html同源檢測的示例: 

URL 結果 原因
http://a.xyz.com/dir2/other.html 成功  
http://a.xyz.com/dir/inner/another.html
成功  
https://a.xyz.com/secure.html 失敗 不同協議 ( https和http )
http://a.xyz.com:81/dir/etc.html 失敗 不同埠 ( 81和80)
http://a.opq.com/dir/other.html 失敗 不同域名 ( xyz和opq)

 

同源策略是什麼

同源策略是瀏覽器的一個安全功能,不同源的客戶端指令碼在沒有明確授權的情況下,不能讀寫對方資源。所以xyz.com下的js指令碼採用ajax讀取abc.com裡面的檔案資料是會被拒絕的。

同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。

不受同源策略限制的

1. 頁面中的連結,重定向以及表單提交是不會受到同源策略限制的。

2. 跨域資源的引入是可以的。但是js不能讀寫載入的內容。如嵌入到頁面中的<script src="..."></script>,<img>,<link>,<iframe>等。

舉個例子

我們手寫兩個Django demo:

demo1

urls.py

urlpatterns = [
    url(r'^abc/', views.abc),
]

views.py

def abc(request):
    return HttpResponse("rion")

demo2

urls.py

urlpatterns = [
    url(r'^xyz/', views.xyz),
]

views.py

def xyz(request):
    return render(request, "xyz.html")

xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8002/abc/",
      type: "get",
      success:function (res) {
        console.log(res);
      }
    })
  });
</script>
</body>
</html>
複製程式碼

現在,開啟使用瀏覽器開啟http://127.0.0.1:8000/xyz/,點選頁面上的 '點我' 按鈕,會在console頁面發現錯誤資訊如下:

為什麼報錯呢?因為同源策略限制跨域傳送ajax請求。

JSONP解決跨域問題

jsonp原理解析

細心點的同學應該會發現我們的demo1專案其實已經接收到了請求並返回了響應,是瀏覽器對非同源請求返回的結果做了攔截。

再細心點的同學會發現,我們使用cdn方式引用的jQuery檔案也是跨域的,它就可以使用。

同樣是從其他的站點拿東西,script標籤就可以。那我們能不能利用這一點搞點事情呢?

把xyz.html中的程式碼改一下:

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

現在,我們重新整理一下頁面,會出現如下錯誤提示:

 

看來後端返回的響應已經被拿到了,只不過把rion當成了一個變數來使用,但是該頁面上卻沒有定義一個名為rion的變數。所以出錯了。

那我定義一個rion變數還不行嗎?

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  var rion = 100;
</script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

這次就不會報錯了。

我定義一個變數可以,那可不可以定義一個函式呢?

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion() {
    console.log("選我不後悔!");
  }
</script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

同時把返回的響應也改一下:

def abc(request):
    return HttpResponse("rion()")

此時,再次重新整理頁面,可以看到下面的結果。

啊,真是讓人性興奮的操作!

我返回的 rion(),頁面上拿到這個響應之後直接執行了rion函式!


 

那函式中可不可以傳遞引數呢?我們試一下!

 demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion(res) {
    console.log(res);
  }
</script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

demo1中的檢視函式:

def abc(request):
    res = {"code": 0, "data": ["SNIS-561", "SNIS-517", "SNIS-539"]}
    return HttpResponse("rion({})".format(json.dumps(res)))

重新整理頁面檢視效果:

果然傳遞引數也是可以的!

我們通過script標籤的跨域特性來繞過同源策略拿到想要的資料了!!!


 

這其實就是JSONP的簡單實現模式,或者說是JSONP的原型:建立一個回撥函式,然後在遠端服務上呼叫這個函式並且將JSON 資料形式作為引數傳遞,完成回撥。

將JSON資料填充進回撥函式,這就是JSONP的JSON+Padding的含義。

但是我們更多時候是希望通過事件觸發資料的獲取,而不是像上面一樣頁面一重新整理就執行了,這樣很不靈活。

其實這很好解決,我們可以通過javascript動態的建立script標籤來實現。

demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion(res) {
    console.log(res);
  }
  function addScriptTag(src){
    var scriptEle = document.createElement("script");
    $(scriptEle).attr("src", src);
    $("body").append(scriptEle);
    $(scriptEle).remove();
  }
  $("#b1").click(function () {
    addScriptTag("http://127.0.0.1:8002/abc/")
  })
</script>
</body>
</html>
複製程式碼

這樣當我們點選b1按鈕的時候,會在頁面上插入一個script標籤,然後從後端獲取資料。

為了實現更加靈活的呼叫,我們可以把客戶端定義的回撥函式的函式名傳給服務端,服務端則會返回以該回調函式名,將獲取的json資料傳入這個函式完成回撥。

demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion(res) {
    console.log(res);
  }

  function addScriptTag(src) {
    var scriptEle = document.createElement("script");
    $(scriptEle).attr("src", src);
    $("body").append(scriptEle);
    $(scriptEle).remove();
  }
  $("#b1").click(function () {
    addScriptTag("http://127.0.0.1:8002/abc/?callback=rion")
  });
</script>
</body>
</html>
複製程式碼

demo1中的views.py

def abc(request):
    res = {"code": 0, "data": ["SNIS-561", "SNIS-517", "SNIS-539"]}
    func = request.GET.get("callback")
    return HttpResponse("{}({})".format(func, json.dumps(res)))

這樣就能實現動態的呼叫了。

jQuery中getJSON方法

jQuery中有專門的方法實現jsonp。

demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.getJSON("http://127.0.0.1:8002/abc/?callback=?", function (res) {
      console.log(res);
    })
  });
</script>
</body>
</html>
複製程式碼

要注意的是在url的後面必須要有一個callback引數,這樣getJSON方法才會知道是用JSONP方式去訪問服務,callback後面的那個?是jQuery內部自動生成的一個回撥函式名。

但是如果我們想自己指定回撥函式名,或者說服務上規定了回撥函式名該怎麼辦呢?我們可以使用$.ajax方法來實現:

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8002/abc/",
      dataType: "jsonp",
      jsonp: "callback",
      jsonpCallback: "rion2"
    })
  });
  function rion2(res) {
    console.log(res);
  }
</script>
</body>
</html>
複製程式碼

不過我們通常都會將回調函式寫在success回撥中:

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8002/abc/",
      dataType: "jsonp",
      success: function (res) {
        console.log(res);
      }
    })
  })
</script>
</body>
</html>
複製程式碼

 CORS解決跨域問題

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

CORS簡介

CORS需要瀏覽器和伺服器同時支援。目前基本上主流的瀏覽器都支援CORS。所以只要後端服務支援CORS,就能夠實現跨域。

簡單請求和非簡單請求介紹

瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

一個請求需要同時滿足以下兩大條件才屬於簡單請求。

複製程式碼
(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
複製程式碼

簡單請求的處理方式

在跨域場景下,當瀏覽器傳送簡單請求時,瀏覽器會自動在請求頭中新增表明請求來源的 Origin 欄位。

我們的後端程式只需要在返回的響應頭中加上 Access-Control-Allow-Origin 欄位,並且把該欄位的值設定為 跨域請求的來源地址或簡單的設定為 * 就可以了。

例如:我們可以在Django中介軟體中的process_response方法來給相應物件新增該欄位。

複製程式碼
from django.utils.deprecation import MiddlewareMixin


class CorsMiddleware(MiddlewareMixin):

    def process_response(self, request, response):
        # 給響應頭加上 Access-Control-Allow-Origin 欄位 並簡單的設定為 *
        response['Access-Control-Allow-Origin'] = '*'
        return response
複製程式碼

 

非簡單請求的處理方式

 我們開發中常用到的那些請求方法是PUT或DELETE,或者Content-Type欄位的型別是application/json的都是非簡單請求。

對於非簡單請求,瀏覽器通常都會在請求之前傳送一次 OPTIONS 預檢 請求。該請求會像後端服務詢問是否允許從當前源傳送請求並且詢問允許的 請求方法請求頭欄位

舉個例子:

我們前端使用axios向後端傳送PUT請求,結果:

看看傳送的具體請求:

解決辦法也很簡單,我們可以在後端簡單的給響應物件新增上 常用請求方法(PUT、DELETE)的支援就可以了。

在上面Django的中介軟體中新增如下程式碼:

複製程式碼
from django.utils.deprecation import MiddlewareMixin


class CorsMiddleware(MiddlewareMixin):

    def process_response(self, request, response):
        # 給響應頭加上 Access-Control-Allow-Origin 欄位 並簡單的設定為 *
        response['Access-Control-Allow-Origin'] = '*'
        if request.method == 'OPTIONS':
            # 允許傳送 PUT 請求
            response['Access-Control-Allow-Methods'] = 'PUT, DELETE'
            # 允許在請求頭中攜帶 Content-type欄位,從而支援傳送json資料
            response['Access-Control-Allow-Headers'] = 'Content-type'
        return response
複製程式碼

使用django-cors-headers

我們這個中介軟體確實能解決目前的CORS跨域問題,但是我們的土方法肯定是不夠嚴謹的,已經有人造好輪子-- django-cors-headers 了。

我們只需要安裝這個包,然後按需要配置一下就可以了。

安裝

pip install django-cors-headers

註冊APP

INSTALLED_APPS = [
    ...
    'app01.apps.App01Config',
    'corsheaders',  # 將 corsheaders 這個APP註冊
]

新增中介軟體

必須放在最前面,因為要先解決跨域的問題。只有允許跨域請求,後續的中介軟體才會正常執行。

複製程式碼
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 新增中介軟體
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
複製程式碼

配置

你可以選擇不限制跨域訪問

CORS_ORIGIN_ALLOW_ALL = True

或者你可以選擇設定允許訪問的白名單

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = (
    # '<YOUR_DOMAIN>[:PORT]',
    '127.0.0.1:8080'
)

更多詳細配置詳細請檢視django-cors-headers專案

同源策略

一個源的定義

如果兩個頁面的協議,埠(如果有指定)和域名都相同,則兩個頁面具有相同的

舉個例子:

下表給出了相對http://a.xyz.com/dir/page.html同源檢測的示例: 

URL 結果 原因
http://a.xyz.com/dir2/other.html 成功  
http://a.xyz.com/dir/inner/another.html 成功  
https://a.xyz.com/secure.html 失敗 不同協議 ( https和http )
http://a.xyz.com:81/dir/etc.html 失敗 不同埠 ( 81和80)
http://a.opq.com/dir/other.html 失敗 不同域名 ( xyz和opq)

 

同源策略是什麼

同源策略是瀏覽器的一個安全功能,不同源的客戶端指令碼在沒有明確授權的情況下,不能讀寫對方資源。所以xyz.com下的js指令碼採用ajax讀取abc.com裡面的檔案資料是會被拒絕的。

同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。

不受同源策略限制的

1. 頁面中的連結,重定向以及表單提交是不會受到同源策略限制的。

2. 跨域資源的引入是可以的。但是js不能讀寫載入的內容。如嵌入到頁面中的<script src="..."></script>,<img>,<link>,<iframe>等。

舉個例子

我們手寫兩個Django demo:

demo1

urls.py

urlpatterns = [
    url(r'^abc/', views.abc),
]

views.py

def abc(request):
    return HttpResponse("rion")

demo2

urls.py

urlpatterns = [
    url(r'^xyz/', views.xyz),
]

views.py

def xyz(request):
    return render(request, "xyz.html")

xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8002/abc/",
      type: "get",
      success:function (res) {
        console.log(res);
      }
    })
  });
</script>
</body>
</html>
複製程式碼

現在,開啟使用瀏覽器開啟http://127.0.0.1:8000/xyz/,點選頁面上的 '點我' 按鈕,會在console頁面發現錯誤資訊如下:

為什麼報錯呢?因為同源策略限制跨域傳送ajax請求。

JSONP解決跨域問題

jsonp原理解析

細心點的同學應該會發現我們的demo1專案其實已經接收到了請求並返回了響應,是瀏覽器對非同源請求返回的結果做了攔截。

再細心點的同學會發現,我們使用cdn方式引用的jQuery檔案也是跨域的,它就可以使用。

同樣是從其他的站點拿東西,script標籤就可以。那我們能不能利用這一點搞點事情呢?

把xyz.html中的程式碼改一下:

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

現在,我們重新整理一下頁面,會出現如下錯誤提示:

 

看來後端返回的響應已經被拿到了,只不過把rion當成了一個變數來使用,但是該頁面上卻沒有定義一個名為rion的變數。所以出錯了。

那我定義一個rion變數還不行嗎?

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  var rion = 100;
</script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

這次就不會報錯了。

我定義一個變數可以,那可不可以定義一個函式呢?

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion() {
    console.log("選我不後悔!");
  }
</script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

同時把返回的響應也改一下:

def abc(request):
    return HttpResponse("rion()")

此時,再次重新整理頁面,可以看到下面的結果。

啊,真是讓人性興奮的操作!

我返回的 rion(),頁面上拿到這個響應之後直接執行了rion函式!


 

那函式中可不可以傳遞引數呢?我們試一下!

 demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion(res) {
    console.log(res);
  }
</script>
<script src="http://127.0.0.1:8002/abc/"></script>
</body>
</html>
複製程式碼

demo1中的檢視函式:

def abc(request):
    res = {"code": 0, "data": ["SNIS-561", "SNIS-517", "SNIS-539"]}
    return HttpResponse("rion({})".format(json.dumps(res)))

重新整理頁面檢視效果:

果然傳遞引數也是可以的!

我們通過script標籤的跨域特性來繞過同源策略拿到想要的資料了!!!


 

這其實就是JSONP的簡單實現模式,或者說是JSONP的原型:建立一個回撥函式,然後在遠端服務上呼叫這個函式並且將JSON 資料形式作為引數傳遞,完成回撥。

將JSON資料填充進回撥函式,這就是JSONP的JSON+Padding的含義。

但是我們更多時候是希望通過事件觸發資料的獲取,而不是像上面一樣頁面一重新整理就執行了,這樣很不靈活。

其實這很好解決,我們可以通過javascript動態的建立script標籤來實現。

demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion(res) {
    console.log(res);
  }
  function addScriptTag(src){
    var scriptEle = document.createElement("script");
    $(scriptEle).attr("src", src);
    $("body").append(scriptEle);
    $(scriptEle).remove();
  }
  $("#b1").click(function () {
    addScriptTag("http://127.0.0.1:8002/abc/")
  })
</script>
</body>
</html>
複製程式碼

這樣當我們點選b1按鈕的時候,會在頁面上插入一個script標籤,然後從後端獲取資料。

為了實現更加靈活的呼叫,我們可以把客戶端定義的回撥函式的函式名傳給服務端,服務端則會返回以該回調函式名,將獲取的json資料傳入這個函式完成回撥。

demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  function rion(res) {
    console.log(res);
  }

  function addScriptTag(src) {
    var scriptEle = document.createElement("script");
    $(scriptEle).attr("src", src);
    $("body").append(scriptEle);
    $(scriptEle).remove();
  }
  $("#b1").click(function () {
    addScriptTag("http://127.0.0.1:8002/abc/?callback=rion")
  });
</script>
</body>
</html>
複製程式碼

demo1中的views.py

def abc(request):
    res = {"code": 0, "data": ["SNIS-561", "SNIS-517", "SNIS-539"]}
    func = request.GET.get("callback")
    return HttpResponse("{}({})".format(func, json.dumps(res)))

這樣就能實現動態的呼叫了。

jQuery中getJSON方法

jQuery中有專門的方法實現jsonp。

demo2中的xyz.html

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.getJSON("http://127.0.0.1:8002/abc/?callback=?", function (res) {
      console.log(res);
    })
  });
</script>
</body>
</html>
複製程式碼

要注意的是在url的後面必須要有一個callback引數,這樣getJSON方法才會知道是用JSONP方式去訪問服務,callback後面的那個?是jQuery內部自動生成的一個回撥函式名。

但是如果我們想自己指定回撥函式名,或者說服務上規定了回撥函式名該怎麼辦呢?我們可以使用$.ajax方法來實現:

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8002/abc/",
      dataType: "jsonp",
      jsonp: "callback",
      jsonpCallback: "rion2"
    })
  });
  function rion2(res) {
    console.log(res);
  }
</script>
</body>
</html>
複製程式碼

不過我們通常都會將回調函式寫在success回撥中:

複製程式碼
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>xyz</title>
</head>
<body>
<button id="b1">點我</button>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
  $("#b1").click(function () {
    $.ajax({
      url: "http://127.0.0.1:8002/abc/",
      dataType: "jsonp",
      success: function (res) {
        console.log(res);
      }
    })
  })
</script>
</body>
</html>
複製程式碼

 CORS解決跨域問題

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

CORS簡介

CORS需要瀏覽器和伺服器同時支援。目前基本上主流的瀏覽器都支援CORS。所以只要後端服務支援CORS,就能夠實現跨域。

簡單請求和非簡單請求介紹

瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

一個請求需要同時滿足以下兩大條件才屬於簡單請求。

複製程式碼
(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
複製程式碼

簡單請求的處理方式

在跨域場景下,當瀏覽器傳送簡單請求時,瀏覽器會自動在請求頭中新增表明請求來源的 Origin 欄位。

我們的後端程式只需要在返回的響應頭中加上 Access-Control-Allow-Origin 欄位,並且把該欄位的值設定為 跨域請求的來源地址或簡單的設定為 * 就可以了。

例如:我們可以在Django中介軟體中的process_response方法來給相應物件新增該欄位。

複製程式碼
from django.utils.deprecation import MiddlewareMixin


class CorsMiddleware(MiddlewareMixin):

    def process_response(self, request, response):
        # 給響應頭加上 Access-Control-Allow-Origin 欄位 並簡單的設定為 *
        response['Access-Control-Allow-Origin'] = '*'
        return response
複製程式碼

 

非簡單請求的處理方式

 我們開發中常用到的那些請求方法是PUT或DELETE,或者Content-Type欄位的型別是application/json的都是非簡單請求。

對於非簡單請求,瀏覽器通常都會在請求之前傳送一次 OPTIONS 預檢 請求。該請求會像後端服務詢問是否允許從當前源傳送請求並且詢問允許的 請求方法請求頭欄位

舉個例子:

我們前端使用axios向後端傳送PUT請求,結果:

看看傳送的具體請求:

解決辦法也很簡單,我們可以在後端簡單的給響應物件新增上 常用請求方法(PUT、DELETE)的支援就可以了。

在上面Django的中介軟體中新增如下程式碼:

複製程式碼
from django.utils.deprecation import MiddlewareMixin


class CorsMiddleware(MiddlewareMixin):

    def process_response(self, request, response):
        # 給響應頭加上 Access-Control-Allow-Origin 欄位 並簡單的設定為 *
        response['Access-Control-Allow-Origin'] = '*'
        if request.method == 'OPTIONS':
            # 允許傳送 PUT 請求
            response['Access-Control-Allow-Methods'] = 'PUT, DELETE'
            # 允許在請求頭中攜帶 Content-type欄位,從而支援傳送json資料
            response['Access-Control-Allow-Headers'] = 'Content-type'
        return response
複製程式碼

使用django-cors-headers

我們這個中介軟體確實能解決目前的CORS跨域問題,但是我們的土方法肯定是不夠嚴謹的,已經有人造好輪子-- django-cors-headers 了。

我們只需要安裝這個包,然後按需要配置一下就可以了。

安裝

pip install django-cors-headers

註冊APP

INSTALLED_APPS = [
    ...
    'app01.apps.App01Config',
    'corsheaders',  # 將 corsheaders 這個APP註冊
]

新增中介軟體

必須放在最前面,因為要先解決跨域的問題。只有允許跨域請求,後續的中介軟體才會正常執行。

複製程式碼
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 新增中介軟體
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
複製程式碼

配置

你可以選擇不限制跨域訪問

CORS_ORIGIN_ALLOW_ALL = True

或者你可以選擇設定允許訪問的白名單

CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = (
    # '<YOUR_DOMAIN>[:PORT]',
    '127.0.0.1:8080'
)

更多詳細配置詳細請檢視django-cors-headers專案