Django
1 中间件介绍
1.1 中间件作用
每个中间件可以理解为项目中的一个独立的功能。
中间件作为Django项目的门户。
- 请求来的时候,需要先经过中间件处理才能到达后端。
- 相应走的时候,需要先经过中间件处理才能发送出去。
只要是涉及到项目全局的功能,首先应该考虑使用中间件。
- 全局用户身份校验;
- 全局用户权限校验;
- 全局访问频率校验。
Django内部有7个内置的中间件。
Django支持自定义中间件,提供了5个可以自定义的方法。
settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
上面相当于从模块中导入类
from django.middleware.security import SecurityMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.middleware.common import CommonMiddleware
from django.middleware.csrf import CsrfViewMiddleware
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.messages.middleware import MessageMiddleware
from django.middleware.clickjacking import XFrameOptionsMiddleware
7个内置的中间件类的特点
- 类内部定义了方法process_xxx;
- 都继承自父类MiddlewareMixin。
1.2 Django请求生命周期流程图
2 中间件方法
2.1 介绍
Django支持自定义中间件,提供了暴露给程序员5个可以自定义的方法。
重要的方法
process_request
process_response
了解的方法
process_view
process_template_response
process_exception
2.2 自定义中间件
创建自定义中间件步骤:
- 在项目根目录或者app目录下创建一个任意名称的文件夹;
- 在该文件夹内创建一个任意名称的py文件;
- 在该py文件内创建一个任意名称的自定义类,该类需要继承MiddlewareMixin;
- 可以在该类内自定义5个方法;
- 在配置文件中注册自定义中间件,将类的路径+类名以字符串的形式(‘路径.类名’)添加到配置文件settings.py中的列表MIDDLEWARE中。
MIDDLEWARE = [
...
'app01.mymiddleware.mymiddleware.MyMiddleware',
]
mymiddleware.py
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_request(self, request):
print('执行自定义中间件的process_request')
2.3 重要的方法(2个)
2.3.1 process_request
process_request
- 请求来的时候需要经过每一个中间件的process_request方法,
顺序是按照配置文件中注册的中间件从上向下依次执行。 - 如果中间件中没有定义方法process_request,直接跳过。
- 如果中间件的方法process_request的返回值是HttpResponse对象,请求将不会继续向后传递,直接原路返回。
因此process_request可用于做全局相关的校验功能,如果校验失败,直接不允许访问。
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_request(self, request):
pass
2.3.2 process_response
process_response
- 响应走的时候需要经过每一个中间件的process_response方法,
顺序是按照配置文件中注册的中间件从下向上依次执行。 - 如果中间件中没有定义方法process_response,直接跳过。
- process_response方法的参数response是后端返回给浏览器的HttpResponse对象,该方法的返回值必须是HttpResponse对象,可以是其参数response。
特点:如果形参中含有response,则返回值必须是一个HttpResponse对象。 - 如果响应来的时候,process_request方法返回了HttpResponse对象,请求将原路返回,就是从同级别的process_response方法开始向外返回。
与django不同,flask中只要返回数据,就必须经过所有的类似于process_response的用于处理返回响应的方法。
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_response(self, request, response):
...
return response
2.4 了解的方法(3个)
process_view
触发时机:路由匹配成功之后,执行视图函数之前。
顺序是按照配置文件中注册的中间件从上向下依次执行。
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_view(self, request, view_name, *args, **kwargs):
pass
process_template_response
触发条件:视图函数返回的HttpResponse对象中有一个render()方法。
顺序是按照配置文件中注册的中间件从下向上依次执行。
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_template_response(self, request, response):
...
return response
views.py
def index(request):
obj = HttpResponse('test1')
def render():
...
return HttpResponse('test2')
obj.render = render
return obj
process_exception
触发条件:视图函数中出现错误抛出异常时。
顺序是按照配置文件中注册的中间件从上向下依次执行。
参数exception是报错提示信息。
from django.utils.deprecation import MiddlewareMixin
class MyMiddleware(MiddlewareMixin):
def process_exception(self, request, exception):
pass
3 csrf跨站请求伪造
跨站请求伪造(csrf,Cross-site Request Forgery)
与钓鱼网站有关。
3.1 介绍钓鱼网站
钓鱼网站本质
用假网站冒充正规网站。
钓鱼网站现象
首先搭建一个与正规网站一模一样的网站,即钓鱼网站,这里以银行网站为例。
用户不小心进入了这个假的银行网站,进行转账操作。
转账操作提交到了真正的银行系统,用户的存款也减少了。
但是转账的目标账户不是用户的目标账户,而是另一个账户。
钓鱼网站核心流程
在钓鱼网站的转账页面中,针对目标账户的输入框,只给用户提供了一个没有name属性的input标签。
在内部使用hidden属性隐藏一个已填好name和value的input标签,name是目标用户字段,value是设定好的账户。
这样用户转账的目标账户实际是内部设定好的账户。
钓鱼网站提交的目标是实际的银行系统,银行系统后端接收到的参数表明用户向钓鱼网站设定的账户进行转账操作。
3.2 跨站请求伪造校验
防止钓鱼网站的思路:后端系统需要判断识别出请求是否来自本后端提供的网页。
跨站请求伪造校验思路
后端系统向用户返回一个具有提交表单功能的页面时,会在页面上添加一个隐藏的唯一标识。
这个唯一标识每次请求都不相同。
页面向后端系统提交表单时会发送这个唯一标识,这样后端系统能够通过唯一标识判断出请求是否合法。
如果请求的唯一标识不存在,后端系统会拒绝此次请求,返回403 Forbidden错误。
Django后端通过内置中间件CsrfViewMiddleware进行csrf校验。
前端提交post请求方式包括:
- form表单
- ajax
3.3 form表单实现csrf校验
<form action="" method="post">
{% csrf_token %}
...
</form>
在form表单中添加{% csrf_token %}
,它会渲染成隐藏的input标签。
<form action="" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="token唯一标识">
...
</form>
3.4 ajax实现csrf校验
方式1,通过查找标签获取页面上的唯一标识。
<script>
$('#btn1').click(function () {
$.ajax({
url: '',
method: 'post',
data: {
...,
'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]'.val())
},
success: function(args) {
}
})
})
</script>
方式2,使用模版语法。
<script>
$('#btn1').click(function () {
$.ajax({
url: '',
method: 'post',
data: {
...,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(args) {
}
})
})
</script>
方式3,引入js文件
方式3不依赖于模版语法,推荐。
static文件夹/js文件夹/mysetup.js
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);
}
}
});
代码详见:Djagno官方文档中关于CSRF的内容
settings.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
{% load static %}
<script src="{% static 'js/mysetup.js' %}"></script>
<script>
$('#btn1').click(function () {
$.ajax({
url: '',
method: 'post',
data: {
...
},
success: function(args) {
}
})
})
</script>
3.5 csrf相关装饰器
3.5.1 介绍
需求:
- 网站整体都不校验csrf(配置文件注释掉csrf校验),只有几个指定的视图函数需要校验;
- 网站整体都校验csrf,只有几个指定的视图函数不进行校验。
csrf相关装饰器
csrf_protect 需要校验
csrf_exempt 忽视校验
3.5.2 FBV
csrf_protect与csrf_exempt使用方式相同。
from django.views.decorators.csrf import csrf_protect, csrf_exempt
@csrf_protect
def test_csrf_protect(request):
pass
@csrf_exempt
def test_csrf_exempt(request):
pass
3.5.3 CBV
对于csrf_protect,有三种方式添加装饰器。
方式1
from django.views.decorators.csrf import csrf_protect
from django.utils.decorators import method_decorator
from django.views import View
class MyCsrfTest(View):
def get(self, request):
pass
@method_decorator(csrf_protect)
def post(self, request):
pass
方式2
from django.views.decorators.csrf import csrf_protect
from django.utils.decorators import method_decorator
from django.views import View
@method_decorator(csrf_protect, name='post')
class MyCsrfTest(View):
def get(self, request):
pass
def post(self, request):
pass
方式3
from django.views.decorators.csrf import csrf_protect
from django.utils.decorators import method_decorator
from django.views import View
class MyCsrfTest(View):
@method_decorator(csrf_protect)
def dispatch(self, request, *args, **kwargs):
return super(MyCsrfTest, self).dispatch(request, *args, **kwargs)
def get(self, request):
pass
def post(self, request):
pass
对于csrf_exempt,只能作用于dispatch方法才能生效。
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.views import View
class MyCsrfTest(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super(MyCsrfTest, self).dispatch(request, *args, **kwargs)
def get(self, request):
pass
def post(self, request):
pass
另一种为dispatch方法添加csrf_exempt装饰器的方式
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.views import View
@method_decorator(csrf_exempt, name='dispatch')
class MyCsrfTest(View):
def dispatch(self, request, *args, **kwargs):
return super(MyCsrfTest, self).dispatch(request, *args, **kwargs)
def get(self, request):
pass
def post(self, request):
pass
5 importlib模块
from target_folder import target_file
print(target_file.value)
另一种导入模块的方式,使用importlib模块以字符串的形式导入指定模块。
使用importlib模块导入的最小单位为文件,不能导入文件内的名字。
import importlib
TARGET_FILE_STR = 'target_folder.target_file'
target_module = importlib.import_module(TARGET_FILE_STR)
print(target_module.value)
from target_folder import target_file
等价于
importlib.import_module('target_folder.target_file')
4 基于中间件的编程思想-功能的插拔式设计
settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
需求:发送消息,发送方式包括邮件、QQ、微信。
方式1
notify.py
def send_email(content):
print(content)
print('发送邮件。')
def send_wechat(content):
print(content)
print('发送微信。')
def send_qq(content):
print(content)
print('发送qq。')
start.py
from notify import *
def send_all(content):
send_email(content)
send_wechat(content)
send_qq(content)
if __name__ == '__main__':
send_all('Test')
此时,如果需要取消qq发送,需要在函数send_all中将函数send_qq注释掉。
但是需要修改源代码,不便于管理人员操作。
方式2,基于中间件的编程思想
创建notify文件夹,内部创建3个功能py文件和1个__init__.py。
byemail.py
class ByEmail():
def __init__(self):
# 发送邮件的准备工作
pass
def send(self, content):
print(content)
print('发送邮件。')
bywechat.py
class ByWechat():
def __init__(self):
# 发送微信的准备工作
pass
def send(self, content):
print(content)
print('发送微信。')
byqq.py
class ByQQ():
def __init__(self):
# 发送QQ的准备工作
pass
def send(self, content):
print(content)
print('发送QQ。')
定义包需要__init__.py,中间件的编程思想的核心
import settings
import importlib
def send_all(content):
for each_path_str in settings.NOTIFY_LIST:
# 1. 获取每一个模块路径以及对于的类名。
module_path, class_name = each_path_str.rsplit('.', maxsplit=1)
# 2. 利用importlib模块以字符串的形式导入模块。
each_module = importlib.import_module(module_path)
# 3. 利用反射获取类
each_class = getattr(each_module, class_name)
# 4. 通过类实例化对象
each_obj = each_class()
# 5. 利用鸭子类型调用对象的send方法。
each_obj.send(content)
在项目根目录下创建start.py和settings.py。
settings.py
NOTIFY_LIST = [
'notify.byemail.ByEmail',
'notify.bywechat.ByWechat',
'notify.byqq.ByQQ',
]
start.py
import notify
notify.send_all('Test')
此时,如果需要取消qq发送,管理员可以去配置文件中将对应的配置注释掉。
如果需要添加skype发送功能,首先需要先创建新的py文件。
byskype.py
class BySkype():
def __init__(self):
# 发送skype的准备工作
pass
def send(self, content):
print(content)
print('发送skype。')
然后去配置文件settings.py中对skype发送功能进行注册。
settings.py
NOTIFY_LIST = [
'notify.byemail.ByEmail',
'notify.bywechat.ByWechat',
'notify.byqq.ByQQ',
'notify.byskype.BySkype', # 注册skype发送功能。
]
这样做,当添加新功能时,不需要修改项目主要的逻辑代码。
思想总结
- 配置文件注册功能;
- 使用importlib模块以字符串的形式动态地导入模块;
- 使用rsplit方法切割模块路径以及对应的类;
- 使用反射、面向对象思想和鸭子类型。