官网:https://docs.djangoproject.com/zh-hans/3.2/ref/csrf/
1、CSRF是什么
跨站请求伪造(CSRF)与跨站请求脚本正好相反。跨站请求脚本的问题在于,客户端信任服务器端发送的数据。跨站请求伪造的问题在于,服务器信任来自客户端的数据。
2、无 CSRF 时存在的隐患
跨站请求伪造是指攻击者通过 HTTP 请求江数据传送到服务器,从而盗取回话的 cookie。盗取回话 cookie 之后,攻击者不仅可以获取用户的信息,还可以修改该 cookie 关联的账户信息。
那么 Django 中 CSRF 验证大体是一个什么样的原理呢?下面通过一个小例子来简单说明一下:
Django 开启 CSRF 中间件(在settings.py中)
1
| 'django.middleware.csrf.CsrfViewMiddleware'
|
在 app01 的 views.py 中写一个简单的后台
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| from django.shortcuts import render,redirect,HttpResponse
def login(request): if request.method == "GET": return render(request,'login.html') elif request.method == "POST": user = request.POST.get('user') pwd = request.POST.get('pwd') if user == 'root' and pwd == "123123": #生成随机字符串 #写到用户浏览器cookie #保存在服务端session中 #在随机字符串对应的字典中设置相关内容 request.session['username'] = user request.session['islogin'] = True if request.POST.get('rmb',None) == '1': #认为设置超时时间 request.session.set_expiry(10) return redirect('/index/') else: return render(request,'login.html') def index(request): #获取当前随机字符串 #根据随机字符串获取对应的信息 if request.session.get('islogin', None): return render(request,'index.html',{'username':request.session['username']}) else: return HttpResponse('please login ') def logout(request): del request.session['username'] request.session.clear() return redirect('/login/')
|
在 templates 中写一个简单的登陆创建两个文件(login.html,index.html)登陆成功跳转 index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login/" method="post"> <input type="text" name="user" /> <input type="text" name="pwd" /> <input type="checkbox" name="rmb" value="1" /> 10s免登录 <input type="submit" value="提交" /> </form> </body> </html>
|
这个时候,我们点击登陆提交,Django 因为无法通过 csrf 验证返回一个 403,原因是 Django 进行了 csrf 验证。csrf 验证其实是对 http 请求中一段随机字符串的验证,那么这段随机字符串从何而来呢?
这个时候我们尝试在 login.html 添加 csrf_token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login/" method="post"> {% csrf_token %} <input type="text" name="user" /> <input type="text" name="pwd" /> <input type="checkbox" name="rmb" value="1" /> 10s免登录 <input type="submit" value="提交" /> </form> </body> </html>
|
这个时候我们再通过浏览器元素审查,就会发现一段新的代码:

Django 在 html 中创建一个基于 input 框 value 值的随机字符串,这个时候我们再次输入后台设置的账号密码,进行提交,会发现提交成功,进入了我们的 index.html 界面当中:
这就是 csrf 的基本原理,如果没有这样一段随机字符串做验证,我们只要在另一个站点,写一个表单,提交到这个地址下,是一样可以发送数据的,这样就造成了极大的安全隐患。而我们新添加的 csrf_token 就是在我们自己的站点中,设置的隐藏参数,用来进行 csrf 验证。
4、Ajax提交 (CSRF)
上面是一个基于 form 表单提交的小例子,csrf 中间件要验证的随机字符串存放在了 form 表单当中,发送到了后台,那么如果我们使用的是 ajax 异步请求,是不是也要传送这样一个类似的字符串呢?答案是肯定的,但这个字符串又该来自哪里?其实它来自cookie,在上面的那个小例子当中,我们打开 F12 元素审查,会发现,在 cookie 中也存放这样一个 csrftoken:

我们可以从 cookie 中获取到这个随机字符串
1
| var csrftoken = $.cookie('csrftoken');
|
这样变量csrftoken取到的就是我们的随机字符串;但是如果后台想要去接收这个随机字符串,也应该需要一个key,那这个key是什么?我们可以通过查找配置文件,通过控制台输出的方式验证这个key:
1 2
| from django.conf import settings print(settings.CSRF_HEADER_NAME)
|
最后输出的是:HTTP_X_CSRFTOKEN
但这里需要注意的是,HTTP_X_CSRFTOKEN 并不是请求头中发送给 Django 真正拿到的字段名,前端发过去真正的字段名是: X-CSRFtoken
那为什么从 Django 的控制台输出会得到 HTTP_X_CSRFTOKEN 呢?其实我们前端的请求头 X-CSRFtoken 发送到后台之后,django会做一个名字处理,在原来的字段名前家一个 HTTP_,并且将原来的小写字符变成大写的,- 会处理成下划线_,所以会有这两个字段的不一样。但本质上他们指向的都是同一个字符串。知道这一点之后,那么我们前端就可以真正地发起含有CSRF请求头的数据请求了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login/" method="post"> {% csrf_token %} <input type="text" name="user" /> <input type="text" name="pwd" /> <input type="checkbox" name="rmb" value="1" /> 10s免登录 <input type="submit" value="提交" /> <input id="btn" type="button" value="按钮"> </form> <script src="/static/jquery-1.12.4.js"></script> <script src="/static/jquery.cookie.js"></script> <script> var csrftoken = $.cookie('csrftoken'); $(function () { $('#btn').click(function () { $.ajax({ url:'/login/', type:"POST", data:{'username':'root','pwd':'123123'}, header:{'X-CSRFtoken':csrftoken}, success:function (arg) { } }) }) }) </script> </body> </html>
|
在页面中点击按钮之后,会发现请求成功!
那么这个时候有人会问,难道所有的 ajax 请求,都需要这样获取一次写进去吗,会不会很麻烦。针对这一点,jquery 的 ajax 请求中为我们封装了一个方法:ajaxSetup,它可以为我们所有的 ajax 请求做一个集体配置,所以我们可以进行如下改造,这样不管你的ajax 请求有多少,都可以很方便地进行 csrf 验证了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script> var csrftoken = $.cookie('csrftoken'); function csrfSafeMethod(method) { return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } });
$(function () { $('#btn').click(function () { $.ajax({ url:'/login/', type:"POST", data:{'username':'root','pwd':'123123'}, success:function (arg) { } }) }) }); </script>
|
5、装饰器配置
在讲完基本的 csrf 验证操作之后,我们还有一个可说的地方。在平时的产品需求当中,并不一定所有的接口验证都需要进行 csrf 验证,而我们之前采用的是在 settings.py 中间件配置进行全局配置,那么如果遇到了不需要开启csrf的时候该怎么办呢?
1 2 3 4 5 6 7 8 9
| from django.views.decorators.csrf import csrf_exempt,csrf_protect @csrf_protect def index(request): pass @csrf_exempt def login(request): pass
|
@csrf_protect 是开启 csrf 验证的装饰器,@csrf_exempt 是关闭 csrf 验证的装饰器。
6、前后端分离项目手动将 csrftoken 写入 cookie 的方法
手动设置,在 view 中添加(经常失效,建议采用 2,3,4 种方法,亲测有效)
1
| request.META["CSRF_COOKIE_USED"] = True
|
手动调用 csrf 中的 get_token(request) 或 rotate_token(request) 方法。
1 2 3 4 5 6 7 8
| from django.middleware.csrf import get_token ,rotate_token def server(request): return render(request, ‘server.html‘)
|
在 HTML 模板中添加
在需要设置 cookie 的视图上加装饰器 ensure_csrf_cookie()
1 2 3 4 5 6
| from django.views.decorators.csrf import ensure_csrf_cookie @ensure_csrf_cookie def server(request): return render(request,'server.html')
|
7、总结
对于 Django 中设置防跨站请求伪造有两种方式:全局和局部。
全局:中间件 Django.middleware.csrf.CsrfViewMiddleware。过滤所有 post 的请求。是为全局的。需要在 html 中加上 { % csrf_token % },Django 会自动生成隐藏的 input 控件,name="csrfmiddlewaretoken"
局部:@csrf_protect,为当前函数强制设置防跨站请求伪造功能,即便 settings 中没有设置全局中间件。设置(局部)也可以通过装饰器来设定局部的 CSRF。
Django 的 CSRF 预防机制总结:Django 第一次响应来自某个客户端的请求时,会在服务器端随机生成一个 token,把这个 token 放在 cookie 里。然后每次 POST 请求都会带上这个 token。如果 POST 请求中没有 token 随机字符串,则返回 403 拒绝服务。
- 对于 POST 表单,需包含一个
csrfmiddlewaretoken 字段。
- 对于 ajax POST 请求,添加一个
X-CSRFTOKEN header,其值为 cookie 里的 csrftoken 的值。