最近在学习JQuery中的AJAX操作,操作本身比较简单,主要是遇到了跨站访问的一些问题,这里详细记录一下原理和解决思路。
Ajax操作
Ajax是一种异步请求服务器数据的技术,可以实现在前端不加载整个网页的情况下更新网页的部分内容。原生的Ajax这里就不讨论了,直接用JQuery,简单而且兼容性强。其中最常用的就是下面的3中方法了
$.ajax()
$.ajax({name:value, name:value, ... })
执行异步ajax请求,参数是一个字典类型,常用的一些字段如下
字段 | 说明 |
---|---|
url | 规定发送请求的 URL,默认是当前页面 |
type | 规定请求的类型(GET 或 POST) |
success(result,status,xhr) | 当请求成功时运行的函数,三个参数分别是返回的结果,返回状态和xhr |
error(xhr,status,error) | 如果请求失败要运行的函数 |
contentType | 发送数据到服务器时所使用的内容类型。默认是:“application/x-www-form-urlencoded” |
data | 规定要发送到服务器的数据 |
dataType | 预期的服务器响应的数据类型 |
$.get()
$.get(URL,data,function(data,status,xhr),dataType)
使用ajax的http get请求从服务器加载数据,这里的参数比较简单,只有URL是必须的,另外几个参数都是可选。
$.post()
$(selector).post(URL,data,function(data,status,xhr),dataType)
使用ajax的http post请求从服务器加载数据,参数和get方法一样。
实际操作
一般情况下用$.ajax()
就能够满足需求了,下面用一个测试页面来实际操作下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="/static/jquery-3.5.1.js"></script>
<title>Test Ajax</title>
<script>
function login(){
$.ajax({
type: "POST",//方法类型
dataType: "json",//预期服务器返回的数据类型
url: "http://172.29.56.178/test/hello/" ,//url
data: $('#form1').serialize(),
success: function (result) {
console.log(result);//打印服务端返回的数据(调试用)
if (result.resultCode == 200) {
alert("SUCCESS");
}
;
},
error : function() {
alert("异常!");
}
});
}
</script>
</head>
<body>
<form action="##" id="form1">
Name:<input type="text" name="userName" placeholder="xiaofu"><br>
Password: <input name="password" type="password"><br>
<input type="button" onclick="login()" value="submit">
</form>
</body>
</html>
页面中一共就一个表单,两个输入框和一个按钮,点击按钮会执行上面定义的login
函数。在函数中执行了一个ajax请求到同网站的/test/hello/
地址,传递的数据是表单数据中的userName
和password
,接受的返回是一个json数据。
同时有必要说明下form表单的按钮,如果是type='submit'
的话表单是会有一个默认的onsubmit
的行为,必须要设置为return false
,不然按钮的点击事件会无法触发,如下
<form action="##" id="form1" onsubmit="return false">
Name:<input type="text" name="userName" placeholder="xiaofu"><br>
Password: <input name="password" type="password"><br>
<input type="submit" onclick="login()" value="submit">
</form>
后端的响应我是用Django写的,view函数如下
@csrf_exempt
def hello(request):
if request.method == 'GET':
response = JsonResponse(data={'status': 'Success', 'message': 'hello from xiaofu'})
elif request.method == 'POST':
name = request.PpasswordOST.get('userName')
password = request.POST.get('password')
response = JsonResponse(data={'status': 'Success', 'name': name, 'password': password, 'message': 'hello from xiaofu'})
return response
因为Django有自带防跨站的csrf中间件,为了不和这里浏览器的跨站混淆,使用装饰器的方式进行了豁免。对Django的csrf中间件感兴趣的朋友可以参考我的另一篇博客《【Django 024】中间件Middleware(三):Django自带csrf中间件源码分析以及跳过csrf报错的四种方法》
后端会获取post上来的userName
和password
字段的内容并放入json返回,测试结果如下
成功通过ajax进行了一次post请求,并且返回了数据。但是注意,这里是用ajax访问的同一个网站的内容。
我这里使用Nginx部署的网站,感兴趣的朋友可以参考这几篇nginx动静分离部署Django项目的教程
浏览器同源策略
之所以在上面要强调是同一网站的内容,是因为如果直接进行跨站访问默认会失败,因为浏览器有一个叫做同源策略(Same Origin Policy)的安全策略在限制这种跨站访问行为。
同源策略是浏览器最核心也最基本的安全功能,也就是说一个网站的脚本只能去请求同一网站的资源。这里的同源指的是:协议,域名和端口都相同。
例如针对网站http://www.xiaofu.com/test/index.html
来说
URL | 是否同源 | 原因 |
---|---|---|
http://www.xiaofu.com/other/index.html | 是 | 协议,域名,端口都一致 |
https://www.xiaofu.com/test/index.html | 否 | 协议不一致 |
http://www.xiaofu.com:8080/test/index.html | 否 | 端口不一致 |
http://www.xiaofu.cn/test/index.html | 否 | 域名不一致 |
那么到底为什么要有这个同源策略的限制存在呢?
因为浏览器在访问某网站的时候会带上与该网站相关的所有cookie信息,假如用户登陆了银行网站http://www.bank.com
,该网站就会像用户使用的浏览器下发用于用户认证的cookie。此时用户访问了钓鱼网站http://www.xiaofu.com
,该网站有一个ajax脚本会在后台访问http://www.bank.com
,此时浏览器会带上用户认证过的cookie,使得银行根本不能分辨是否是用户本人在访问,从而造成隐私信息泄露。而且可怕的是因为ajax在后台可以直接运行,用户甚至都感觉不到自己的信息已经泄露。
跨域操作
下面用实际操作来感受下浏览器的同源策略。
将刚才的前端页面搬到本地127.0.0.1
来,同样是访问相同的后端,此时在浏览器的console中就会有如下报错出现
这里提到的cors
就是其中一种跨站访问的方法
CORS
全称为Cross Origin Resource Sharing,跨域资源共享。它是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10,所以关键是看服务器端是否支持。
先来看看流程再来尝试实现。
浏览器发现是在做一个跨域请求的时候,会在请求头中添加一个Origin
字段,包含发起请求网站的源信息(协议,域名和端口),例如Origin:http://www.xiaofu.com
;浏览器会去检查请求头的Origin
字段,如果允许访问,就会在返回头中添加一个Access-Control-Allow-Origin
字段,返回相同的源信息,例如Access-Control-Allow-Origin:http://www.xiaofu.com
,当然也可以回一个通配符,表示对所有网站都开放访问;如果服务器返回的源信息和发送的源信息不符合,则会出现上面的报错。
下面是刚才发送的请求头信息,可以看到浏览器发送了Origin
字段
再看服务器返回的头信息,并没有Access-Control-Allow-Origin
字段
下面修改下服务器的view函数,给返回设置头信息
@csrf_exempt
def hello(request):
if request.method == 'GET':
response = JsonResponse(data={'status': 'Success', 'message': 'hello from xiaofu'})
response['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8848'
elif request.method == 'POST':
name = request.POST.get('userName')
password = request.POST.get('password')
response = JsonResponse(data={'status': 'Success', 'name': name, 'password': password, 'message': 'hello from xiaofu'})
response['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8848'
return response
之后发现可以成功跨站访问,并且从浏览器查看返回头也看到了Access-Control-Allow-Origin
信息
值得一提的是这种跨域方式是不带敏感信息的,例如cookie信息。要携带敏感信息,还需要在ajax请求中设置xhr的属性 withCredentials 为 true,同时还需要服务器返回的头信息中有Access-Control-Allow-Credentials: true
信息。因为cookie话题展开会较大,下次单独再写博客说明。
那么如果人为将请求网站的源信息修改一下可不可以呢?
可以,通过document.domain
就可以修改。但是不能随便修改,只能修改为自身或者更高一级的父域才可以。例如http://www.xiaofu.com
只能修改为http://xiaofu.com
。同时目标页面也要显性设置为同样的domain才可以跨域访问。所以这种方式一般用于主域名相同,而子域名不同的情况。这里也不展开说明了。
JSONP
细心的朋友可能想到了,我们在页面中引入外部的js脚本,css样式或者是图片都不受上面的同源策略的限制。那么就可以在script标签的src属性中进行跨站访问。
JSONP(JSON with Padding)是一种跨站发送JSON数据的方法,使用<script>
标签的src
属性来请求数据,并且请求路径必须带上callback
字段。服务器在返回的时候将数据作为参数包裹在callback函数调用中,客户端收到返回之后调用本地的同名函数,该函数必须在客户端已存在才行。但是要注意,该方式只支持GET方式。
下面来实际操作一下。
首先把前面服务端设置的CORS返回头给注释掉,确保直接访问会出错。然后修改下前端如下
<script type="text/javascript">
function handleJson(data){
console.log(data);
}
function login(){
var name = $('#userName').val();
var passwd = $('#password').val();
var $newScript = $("<script><\/script>");
$newScript.attr('src','http://172.29.56.178/test/hello/?callback=handleJson&userName='+name+'&password='+passwd);
$('body').append($newScript);
}
</script>
注意创建script标签时候的那个转义符号
这里声明一个函数handleJson
用于处理服务器返回的数据,同时修改了login
函数的逻辑,新建一个script标签,同时将表单里面的内容以查询参数的形式传递给后端(因为只能用GET方式),并且附上callback=handleJson
的回调函数信息。
在服务端,修改Django的view函数如下
@csrf_exempt
def hello(request):
if request.method == 'GET':
if 'callback' in request.GET:
name = request.GET.get('userName')
password = request.GET.get('password')
data = {'status': 'Success', 'name': name, 'password': password, 'message': 'hello from xiaofu'}
result = '{}({})'.format(request.GET.get('callback'), data)
response = HttpResponse(result, content_type="application/json")
elif request.method == 'POST':
name = request.POST.get('userName')
password = request.POST.get('password')
response = JsonResponse(
data={'status': 'Success', 'name': name, 'password': password, 'message': 'hello from xiaofu'})
return response
注意这里返回的是callback(data)
的形式,同时指明content_type
为json。
成功拿到结果
并且没有任何CORS相关的头信息
总结
总结下这一节的知识点
- JQuery中有3种常用的ajax请求方式,基本用
$.ajax()
方式即可 - 浏览器出于安全考虑,默认情况下只允许页面脚本在同源站点间进行互相访问。同源指的是协议,域名和端口都一致
- 如果浏览器和服务器都支持CORS协议,通过在请求头和响应头里面添加源的信息即可实现跨域访问,GET和POST方式都行
- 源信息可以被修改,但是只能是同一级或者是父级域。同时必须请求页面和目标页面都显性设置。适用于主域名相同的子域名之间的访问
- script标签的src因为不受浏览器的同源策略限制,可以实现JSONP的跨域操作。这种方式只支持GET方式,同时必须客户端和服务器的callback函数名要一致
当然,这里对于跨域操作只是抛砖引玉,还有很多种跨域操作。感兴趣的朋友可以参考下面两篇博客
而关于跨域携带cookie的操作,我会在下一篇博客给大家进行分享。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。