同源策略和跨域解決方案

同源策略

一個源的定義

若是兩個頁面的協議,端口(若是有指定)和域名都相同,則兩個頁面具備相同的javascript

舉個例子:css

下表給出了相對http://a.xyz.com/dir/page.html同源檢測的示例: 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裏面的文件數據是會被拒絕的。前端

同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。java

不受同源策略限制的

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

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

舉個例子

咱們手寫兩個Django demo:ios

demo1

urls.pygit

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

views.pygithub

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請求。

細心點應該會發現咱們的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",
    jsonp: "callbacks", success:
function (res) { console.log(res); } }) }) </script> </body> </html>

二、CORS

隨着技術的發展,如今的瀏覽器能夠支持主動設置從而容許跨域請求,即:跨域資源共享(CORS,Cross-Origin Resource Sharing),其本質是設置響應頭,使得瀏覽器容許跨域請求。

* 簡單請求 OR 非簡單請求

1
2
3
4
5
6
7
8
9
10
11
12
13
條件:
     1 、請求方式:HEAD、GET、POST
     2 、請求頭信息:
         Accept
         Accept - Language
         Content - Language
         Last - Event - ID
         Content - Type  對應的值是如下三個中的任意一個
                                 application / x - www - form - urlencoded
                                 multipart / form - data
                                 text / plain
 
注意:同時知足以上兩個條件時,則是簡單請求,不然爲複雜請求

* 簡單請求和非簡單請求的區別?

1
2
    簡單請求:一次請求
非簡單請求:兩次請求,在發送數據以前會先發一次請求用於作「預檢」,只有「預檢」經過後纔再發送一次請求用於數據傳輸。

* 關於「預檢」

1
2
3
4
5
6
7
-  請求方式:OPTIONS
-  「預檢」其實作檢查,檢查若是經過則容許傳輸數據,檢查不經過則再也不發送真正想要發送的消息
-  如何「預檢」
      = > 若是複雜請求是PUT等請求,則服務端須要設置容許某請求,不然「預檢」不經過
         Access - Control - Request - Method
      = > 若是複雜請求設置了請求頭,則服務端須要設置容許某請求頭,不然「預檢」不經過
         Access - Control - Request - Headers

 

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項目

相關文章
相關標籤/搜索