同源策略和跨域請求解決方案
一、一個源的定義
如果兩個頁面的協議,埠(如果有指定)和域名都相同,則兩個頁面具有相同的源。
舉個例子:
下表給出了相對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裡面的檔案資料是會被拒絕的。
同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。
三、基於jsonp實現的跨域請求
-
頁面中的連結,重定向以及表單提交是不會受到同源策略限制的。
-
跨域資源的引入是可以的。但是js不能讀寫載入的內容。如嵌入到頁面中的<script src="..."></script>,<img>,<link>,<iframe>等。
下面來分步舉例詳細闡述其中的奧妙:
1、先開兩個專案,
專案1(http://127.0.0.1:8000/)
專案2(http://127.0.0.1:8100/)
專案1
url: url(r'index1/$',views.index1) views: def index1(request): returnHttpResponse('wangjifei')
專案2
url: url(r'index2/$',views.index2) views: def index2(request): return render(request,'index2.html') index2.html: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> $('#btn').click(function () { $.ajax({ url:"http://127.0.0.1:8000/index1/", type:'get', success:function (res) { console.log(res) } }) }) </script> </body> </html>
現在,開啟使用瀏覽器開啟 ofollow,noindex">http://127.0.0.1:8100/index2/ ,點選頁面上的 '提交' 按鈕,會在console頁面發現錯誤資訊如下:

微信圖片_20180823142349.png
為什麼報錯呢?因為同源策略限制跨域傳送ajax請求。
細心點的同學應該會發現我們的demo1專案其實已經接收到了請求並返回了響應,是瀏覽器對非同源請求返回的結果做了攔截。
再細心點的同學會發現,我們使用cdn方式引用的jQuery檔案也是跨域的,它就可以使用。
同樣是從其他的站點拿東西,script標籤就可以。那我們能不能利用這一點搞點事情呢?
2、把index2.html中的程式碼改一下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script src="http://127.0.0.1:8000/index1/"></script> </body> </html>
現在重新整理一下會出現如下錯誤:

微信圖片_20180823143053.png
看來後端返回的響應已經被拿到了,只不過把wangjifei當成了一個變數來使用,但是該頁面上卻沒有定義一個名為wangjifei的變數。所以出錯了。
3、那我們就在index2.html中定義一個wangjifei變數看看:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script> var wangjifei = 123 </script> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script src="http://127.0.0.1:8000/index1/"></script> </body> </html>
刷新發現不報錯了,

微信圖片_20180823143436.png
4、我定義一個變數可以,那可不可以定義一個函式呢?
index2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script> function wangjifei() { console.log('出手就要專業') } </script> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script src="http://127.0.0.1:8000/index1/"></script> </body> </html>
專案1中的views:也修改一下
def index1(request): returnHttpResponse('wangjifei()')
重新整理一下頁面顯示結果:

微信圖片_20180823145716.png
結果分析:返回的 wangjifei(),頁面上拿到這個響應之後直接執行了wangjifei函式!
5、那函式中可不可以傳遞引數呢?我們試一下!
index2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script> function wangjifei(res) { console.log(res) } </script> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script src="http://127.0.0.1:8000/index1/"></script> </body> </html>
專案1中的 views
from django.http import HttpResponse import json def index1(request): ret={'code':1,'msg':[110,119,120,12306]} res = json.dumps(ret) returnHttpResponse(f'wangjifei({res})')
重新整理之後顯示結果:

微信圖片_20180823151309.png
果然傳遞引數也是可以的!我們通過script標籤的跨域特性來繞過同源策略拿到想要的資料了!!!
這其實就是JSONP的簡單實現模式,或者說是JSONP的原型:建立一個回撥函式,然後在遠端服務上呼叫這個函式並且將JSON 資料形式作為引數傳遞,完成回撥。
將JSON資料填充進回撥函式,這就是JSONP的JSON+Padding的含義。
但是我們更多時候是希望通過事件觸發資料的獲取,而不是像上面一樣頁面一重新整理就執行了,這樣很不靈活。
6、我們可以通過javascript動態的建立script標籤來實現。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> //自定義的函式 function wangjifei(res) { console.log(res) } //jquery給button繫結點選事件 $('#btn').click(function () { //建立一個script標籤 var scriptEle = document.createElement('script'); //給標籤新增src屬性,並新增對應的屬性值http://127.0.0.1:8000/index1 $(scriptEle).attr('src','http://127.0.0.1:8000/index1'); //將建立好的標籤新增到頁面中,標籤新增後就會自動觸發get請求 $('body').append(scriptEle); //將標籤移除 $(scriptEle).remove() }) </script> </body> </html>
這樣當我們點選button按鈕的時候,會在頁面上插入一個script標籤,然後從後端獲取資料後再刪除掉。
7、為了實現更加靈活的呼叫,我們可以把客戶端定義的回撥函式的函式名傳給服務端,服務端則會返回以該回調函式名,將獲取的json資料傳入這個函式完成回撥。這樣就能實現動態的呼叫了。修改程式碼如下:
index2.html程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> //自定義的函式 function xxx(res) { console.log(res) } //jquery給button繫結點選事件 $('#btn').click(function () { //建立一個script標籤 var scriptEle = document.createElement('script'); //給標籤新增src屬性,並新增對應的屬性值http://127.0.0.1:8000/index1?callback=xxx $(scriptEle).attr('src','http://127.0.0.1:8000/index1?callback=xxx'); //將建立好的標籤新增到頁面中,標籤新增後就會自動觸發get請求 $('body').append(scriptEle); //將標籤移除 $(scriptEle).remove() }) </script> </body> </html>
專案1中views:
from django.http import HttpResponse import json def index1(request): ret={'code':1,'msg':[110,119,120,12306]} res = json.dumps(ret) callback = request.GET.get('callback') returnHttpResponse(f'{callback}({res})')
四、jQuery中getJSON方法介紹:
1、jQuery中有專門的方法實現jsonp。
index2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> //jquery給button繫結點選事件 $('#btn').click(function () { $.getJSON("http://127.0.0.1:8000/index1?callback=?",function (res) { console.log(res) }) }) </script> </body> </html>
要注意的是在url的後面必須要有一個callback引數,這樣getJSON方法才會知道是用JSONP方式去訪問服務,callback後面的那個?是jQuery內部自動生成的一個回撥函式名。
2、但是如果我們想自己指定回撥函式名,或者說服務上規定了回撥函式名該怎麼辦呢?我們可以使用$.ajax方法來實現:
index2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> //jquery給button繫結點選事件 $('#btn').click(function () { $.ajax({ //要訪問的url url:"http://127.0.0.1:8000/index1/", //要處理的資料型別jsonp dataType:'jsonp', //自定義回撥函式名必要引數 jsonp:'callback', //自定義回撥函式名,url中callback=後面的函式名 jsonpcallback:'wangjifei' }) }); //回撥函式 function wangjifei(res) { console.log(res) } </script> </body> </html>
views:
from django.http import HttpResponse import json def index1(request): ret={'code':1,'msg':[110,119,120,12306]} res = json.dumps(ret) callback = request.GET.get('callback') returnHttpResponse(f'wangjifei({res})')
3、用ajax技術通常將回調函式寫在成功回撥函式的位置:
index2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="btn">提交</button> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> //jquery給button繫結點選事件 $('#btn').click(function () { $.ajax({ //要訪問的url url:"http://127.0.0.1:8000/index1/", //要處理的資料型別jsonp dataType:'jsonp', //success回撥 success:function (res) { console.log(res) } }) }); //回撥函式 function wangjifei(res) { console.log(res) } </script> </body> </html>
最後來一個jsonp的實際應用:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>同源策略</title> </head> <body> <button id="show-tv">提交</button> <div class="tv-list"></div> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> $("#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; console.log(weekList); 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>"); }) } }) }); </script> </body> </html>
五、基於Core方法解決跨域請求
-
我們介紹了jsonp解決跨域請求問題,這種解決方式很好的詮釋了跨域請求的本質,但是略顯麻煩,是否還記得在我們不做任何處理的時候,跨域請求時候瀏覽器給我們報的錯誤不?翻譯過來就是因為響應頭沒有指定Access-Control-Allow-Origin所允許原始的請求路徑,因此原始請求路徑http://127.0.0.1:8001不被允許訪問。 基於上述的原因解釋,我們只需要在響應的內容加入上述這樣的授權欄位,便可解決。
-
簡單請求的定義:
只要同時滿足以下兩大條件,就屬於簡單請求,不滿足就是複雜請求!!!
1.(1) 請求方法是以下三種方法之一:
-- HEAD,GET,POST
2.(2)HTTP的頭資訊不超出以下幾種欄位:
-- Accept
-- Accept-Language
-- Content-Language
-- Last-Event-ID
-- Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
由於django的所有請求響應都要走中介軟體,所以可以寫一個跨域的中介軟體來解決跨域問題
from django.utils.deprecation import MiddlewareMixin class MyCore(MiddlewareMixin): def process_response(self, request, response): response['Access-Control-Allow-Origin'] = "*"//簡單請求 if request.method == "OPTIONS": # 複雜請求 預檢 response['Access-Control-Allow-Headers'] = "Content-Type" response['Access-Control-Allow-Methods'] = "POST, DELETE, PUT" return response