同源策略
一個源的定義
如果兩個頁面的協議,端口(如果有指定)和域名都相同,則兩個頁面具有相同的源。
舉個例子:
下表給出了相對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項目