同源策略和跨域解決方案
同源策略
一個源的定義
如果兩個頁面的協議,端口(如果有指定)和域名都相同,則兩個頁面具有相同的源。
舉個例子:
下表給出了相對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請求。
細心點的同學應該會發現我們的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>
JSONP應用
// 跨域請求示例 $("#show-tv").click(function () { $.ajax({ url: "http://www.jxntv.cn/data/jmd-jxtv2.html?callback=list&_=1454376870403", dataType: 'jsonp', jsonp: 'callback', jsonpCallback: 'list', success: function (data) { var weekList = data.data; var $tvListEle = $(".tv-list"); $.each(weekList, function (k, v) { var s1 = "<p>" + v.week + "列表</p>"; $tvListEle.append(s1); $.each(v.list, function (k2, v2) { var s2 = "<p><a href='" + v2.link + "'>" + v2.name + "</a></p>"; $tvListEle.append(s2) }); $tvListEle.append("<hr>"); }) } }) });