中间件与CSRF
django中间件
django的请求生命周期流程中,从wsgiref网关接口到路由层需要经过一个中间件,django自带七个中间件,每个都有各自对应的功能,进入settings.py配置文件就可以看到这七个中间件。
除了这七个中间件,django还支持自定义中间件并提供五个可以自定义的方法:process_request、process_response、process_view、process_template_response、process_excepton,它们分别会在不同的时期执行。
自定义中间件
在我们编写自定义中间件时,需要进行如下操作:
- 在应用下创建一个任意名称的文件夹
- 在该文件夹内创建一个任意名称的py文件
- 在该py文件内编写中间件类
- 编写完后要在配置文件中注册
比如我在应用名app01下先创建文件夹middleware,然后在文件夹下创建middle.py,最后在py文件中创建中间件类MyMiddle,这个注册就是这么写:
MIDDLEWARE = [
七个自带中间件,
# 自定义中间件注册
'app01.middleware.middle.MyMiddle'
]
常用的方法
1.process_request
客户端发送请求时,会从上往下依次执行配置文件中注册了的中间件里面的process_request方法,如果没有则直接跳过。
如果该方法自己返回了HttpResponse对象,那么请求不再继续往后直接返回相应的数据,也就是说浏览器就会直接接收该方法返回的HttpResponse对象,不在执行视图函数代码。
from django.shortcuts import HttpResponse
from django.utils.deprecation import MiddlewareMixin
class MyMiddle(MiddlewareMixin):
def process_request(self, request):
return HttpResponse('from MyMiddle process_request')
这个方法可以用于校验用户是否在黑名单之中之类的事务。
2.process_response
服务端响应客户端时,会从下往上依次执行配置文件中注册了的中间件里面的process_response方法,如果没有则直接跳过。
如果该方法自己返回了HttpResponse对象,那么响应会替换成该HttpResponse对象数据,而不再是视图函数想要返回给客户端的数据。
from django.shortcuts import HttpResponse
from django.utils.deprecation import MiddlewareMixin
class MyMiddle(MiddlewareMixin):
# response就是视图函数返回给客户端的数据
def process_response(self, request, response):
return HttpResponse('from MyMiddle process_request')
不常用的方法
1.process_view
路由匹配成功之后,执行视图函数之前,从上往下执行配置文件中注册了的中间件里面的process_view方法。
如果该方法自己返回了HttpResponse对象,那么不再执行视图函数,而是直接返回给浏览器。
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class MyMiddle(MiddlewareMixin):
def process_view(self, request, view_func, view_args, view_kwargs):
print('视图函数:', view_func)
print('视图函数位置参数', view_args)
print('视图函数关键字参数', view_kwargs)
return HttpResponse('from MyMiddle process_view')
2.process_template_response
视图函数执行完毕之后返回的对象中的render属性对应一个函数方法,则会从下往上执行配置文件中注册了的中间件里面的process_template_response方法。
视图函数:
def index(request):
res = HttpResponse()
def abc():
return HttpResponse('abc')
res.render = abc
return res
中间件:
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class MyMiddle(MiddlewareMixin):
def process_template_response(self, request, response):
return response
3.process_exception
视图函数执行过程中报错并在返回响应的时,会从下往上执行配置文件中注册了的中间件里面的process_exception。
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse
class MyMiddle(MiddlewareMixin):
def process_exception(self, request, exception):
print('异常:',exception)
return HttpResponse('报错了')
通过字符串导入模块
通过中间件的配置文件看到:
很显然这些都是一个个模块,这种通过字符串导入模块的方式是因为使用了importlib模块,这个模块可以把字符串的路径导入。
导入模块
import importlib
使用:
module_path = 'aaa.c'
importlib.import_module(module_path)
等价于常规导入方式:
from aaa import c
这个模块有一个缺点:最小导入单位是模块文件级别,也就是无法导入模块文件名称空间中的名称。既无法单独导入py文件中的变量名、函数名、类名等。
基于中间件注册思想编写项目
我们编写完自定义中间件后需要去配置文件中进行注册,这个功能就会自动添加到项目中,那么我们该如果实现这种相同效果呢?
现在想要实现一个功能:不动运行文件,只创建一个方法,并把这个方法配置在配置文件中,就可以执行内部代码。
我们已发送信息模拟。
首先创建如下文件:
email.py:
class Email:
def send(self, msg):
print('邮箱发送了:%s' % msg)
phone.py:
class Phone:
def send(self, msg):
print('短信发送了:%s' % msg)
qq.py:
class QQ:
def send(self, msg):
print('QQ发送了:%s' % msg)
settings.py:
SETTINGS = [
'aaa.email.Email',
'aaa.phone.Phone',
'aaa.qq.QQ',
]
start.py:
import importlib
from settings import SETTINGS
def send_all(msg):
# 1.循环配置文件中的路径
for str_path in SETTINGS:
# 2.切割路径,比如'aaa.qq.QQ'切割成'aaa.qq'与'QQ'
module_name, str_class_name = str_path.rsplit('.', maxsplit=1)
# 3.导入模块
module = importlib.import_module(module_name)
# 4.利用反射获取模块内名称空间的名称
class_name = getattr(module, str_class_name)
# 5.实例化
obj = class_name()
# 6.调用对象里面的方法
obj.send(msg)
if __name__ == '__main__':
send_all('666')
CSRF跨站请求伪造
CSRF跨站请求伪造,简单地说,就是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。
比如说攻击者建立了一个盗版页面,用户在这个盗版页面上输入信息后提交,这个盗版页面会向正版页面的服务器发送请求以实现一些操作,因为用户曾经在正版页面登录过,正版页面认证过这个用户,所以不会阻拦这个用户的请求。
或者说此时有一个正版页面是转账页面,这时攻击者也造了一个盗版页面模仿正版页面,用户如果在盗版页面输入收账用户时,用户输入的收账用户是无效的,因为真实的收账用户被隐藏了,用户一旦提交转账后,就会给被隐藏的收账用户转钱。
正版网站
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>我是正规的网站</h2>
<form action="" method="post">
<p>当前账户:<input type="text" name="current_user"></p>
<p>目标账户:<input type="text" name="target_user"></p>
<p>转账金额:<input type="text" name="money"></p>
<input type="submit">
</form>
</body>
</html>
views:
def home(request):
if request.method == 'POST':
current_user = request.POST.get('current_user')
target_user = request.POST.get('target_user')
money = request.POST.get('money')
return HttpResponse(f'{current_user} 给 {target_user} 转了{money}')
return render(request, 'home.html')
盗版网站
home.html:form表单是向正版网站发送的请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>我是盗版的网站</h2>
<form action="http://127.0.0.1:8000/home" method="post">
<p>当前账户:<input type="text" name="current_user"></p>
<p>目标账户:<input type="text"></p>
<input type="hidden" value="攻击者账户" name="target_user">
<p>转账金额:<input type="text" name="money"></p>
<input type="submit">
</form>
</body>
</html>
views
def home(request):
return render(request, 'home.html')
CSRF解决策略
首先我们先确保如下红框内的中间件存在
解决方法一:表单使用模板语法{% csrf_token %}
然后再正版页面的表单中加上一行模板语法{% csrf_token %}
<form action="" method="post">
{% csrf_token %}
<p>当前账户:<input type="text" name="current_user"></p>
<p>目标账户:<input type="text" name="target_user"></p>
<p>转账金额:<input type="text" name="money"></p>
<input type="submit">
</form>
此时的正版页面就会多了一个隐藏的input标签,这个标签就可以用来标识页面。这时候盗版页面就无法向正版服务器发送请求了,或者说发送请求时被拒绝了。
解决方法二:ajax使用模板语法{% csrf_token %}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
</head>
<body>
<h2>我是正规的网站</h2>
<form action="" method="post">
{% csrf_token %}
<p>当前账户:<input type="text" name="current_user"></p>
<p>目标账户:<input type="text" name="target_user"></p>
<p>转账金额:<input type="text" name="money"></p>
<input id="b1" type="button" value="提交">
</form>
<script>
$('#b1').click(function () {
$.ajax({
url: '',
type: 'post',
data: {
'current_user': $('input[name="current_user"]').val(),
'target_user': $('input[name="target_user"]').val(),
'money': $('input[name="money"]').val(),
'csrfmiddlewaretoken':$('input[name="csrfmiddlewaretoken"]').val()
},
success: function (args) {
alert(args)
}
})
})
</script>
</body>
</html>
ajax携带数据:
'csrfmiddlewaretoken':$('input[name="csrfmiddlewaretoken"]').val()
还可以这么写:
'csrfmiddlewaretoken':{{ csrf_token }}
解决方法三:引入js文件
引人以下js:只适用于ajax提交,引入后ajax无需携带解决方法二里的csrfmiddlewaretoken数据。
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
CSRF相关装饰器
关于CSRF有两个装饰器:
from django.views.decorators.csrf import csrf_exempt, csrf_protect
- csrf_exempt:用于忽略CSRF校验的装饰器
- csrf_protect:用于开启CSRF校验的装饰器
csrf_exempt
这个一般用于使用了CSRF的中间件的时候,因为使用了CSRF中间件相当于给所有视图函数开启了CSRF校验,这个装饰器就是为了给某个视图函数忽略CSRF校验用的。
csrf_protect
这个一般用于没有使用CSRF的中间件的时候,因为所有视图函数都没有开启了CSRF校验,这个装饰器就是为了给某个视图函数开启CSRF校验用的。
针对FBV使用
两个装饰器都可以直接在函数上方添加
@csrf_exempt
def index(request):
return HttpResponse()
针对CBV使用
csrf_protect:三种CBV添加装饰器的方式都可以
csrf_exempt:只有给重写的dispatch方法装饰可以生效
class MyView(views.View):
def get(self, request):
return HttpResponse()
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)