【转】同源策略和跨域请求解决方案
一、一个源的定义
如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。
举个例子:
下表给出了相对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): return HttpResponse('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>
现在,打开使用浏览器打开 http://127.0.0.1:8100/index2/,点击页面上的 '提交' 按钮,会在console页面发现错误信息如下:
为什么报错呢?因为同源策略限制跨域发送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>
现在刷新一下会出现如下错误:
看来后端返回的响应已经被拿到了,只不过把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>
刷新发现不报错了,
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): return HttpResponse('wangjifei()')
刷新一下页面显示结果:
结果分析:返回的 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) return HttpResponse(f'wangjifei({res})')
刷新之后显示结果:
果然传递参数也是可以的!我们通过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') return HttpResponse(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') return HttpResponse(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