Django-Part6——中间件与Auth模块

[TOC]

Django中间件

中间件简介

  • django自带七个中间件,并且支持程序员自定义中间件
  • 只要是涉及到全局相关的功能都可以使用中间件完成
  • 全局用户身份、权限校验
    • 全局访问频率校验
  • django中间件是django的门户
    • 请求需要先经过中间件才能到达django后端
    • 响应也需要经过中间件才能发送出去

自定义中间件

  • 中间件可以在项目名或者应用名下任意名称的文件夹中定义

  • 自定义中间件类必须继承于MiddlewareMixin类

  • 需要将类的路径以字符串的形式注册到配置文件中才能生效

  • 中间件将暴露五个可以自定义的方法,按需求重写

    • process_request(self, request)

      1. 请求来的时候需要经过每一个中间件里面的process_request方法

      2. 顺序是按照配置文件中注册的中间件从上往下的顺序依次执行

      3. 如果中间件里面没有定义该方法,则跳过

      4. 如果该方法返回了HttpResponse对象,则请求将不再继续往后执行,而是从同级别的process_reponse向上原路返回

        flask中即使中途返回数据,但依然经过所有中间件

      process_request方法可以用来做全局相关的所有限制功能

    • process_response(self, request, response)

      1. 响应走的时候需要经过每一个中间件里面的process_response方法
      2. 顺序是按照配置文件中注册了的中间件从下往上依次经过
      3. 如果中间件里面没有定义该方法,则跳过
      4. 该方法必须返回一个HttpResponse对象,默认返回的就是形参response
    • process_view(self, request, view_func, view_args, view_kwargs)

      1. 触发时间在路由匹配成功之后,执行视图函数之前
      2. 顺序是按照配置文件中注册的中间件从上往下的顺序依次执行
    • process_template_response(self, request, response)

      1. 返回的HttpResponse对象有render属性的时候才会触发
      2. 顺序是按照配置文件中注册了的中间件从下往上依次经过
    • process_exception(self, request, exception)

      1. 当视图函数中出现异常的情况下触发
      2. 顺序是按照配置文件中注册了的中间件从下往上依次经过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.utils.deprecation import MiddlewareMixin


class MD1(MiddlewareMixin):
def process_request(self, request):
print("MD1里面的 process_request")

def process_response(self, request, response):
print("MD1里面的 process_response")
return response

def process_view(self, request, view_func, view_args, view_kwargs):
print("-" * 80)
print("MD1 中的process_view")
print(view_func, view_func.__name__)

def process_exception(self, request, exception):
print(exception)
print("MD1 中的process_exception")
return HttpResponse(str(exception))

def process_template_response(self, request, response):
print("MD1 中的process_template_response")
return response

csrf跨站请求伪造

使用cookie完成csrf校验

  • 为了确保请求来源是本服务器发送出去的
  • 为每一次发出去的页面提供唯一标识,如果校验标识失败则直接拒绝(403 forbbiden)
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
<script src="get请求url"></script>

// Ajax CSRF in cookies start
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);
}
}
});
// Ajax CSRF in cookies end
  1. 如果使用从cookie中取csrftoken的方式,需要确保cookie存在csrftoken值。
  2. 如果你的视图渲染的HTML文件中没有包含 {% csrf_token %},Django可能不会设置CSRFtoken的cookie。
  3. 这个时候需要使用ensure_csrf_cookie()装饰器强制设置Cookie。
1
2
3
4
5
django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def login(request):
pass

csrf相关装饰器

  • @csrf_protect:需要校验
  • @csrf_exempt:忽视校验
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
from django.views.decorators.csrf import csrf_protect, csrf_exempt
from django.utils.decorators import method_decorator

# @csrf_exempt
# @csrf_protect
def transfer(request):
if request.method == 'POST':
username = request.POST.get('username')
target_user = request.POST.get('target_user')
money = request.POST.get('money')
print('%s给%s转了%s元'%(username,target_user,money))
return render(request,'transfer.html')


from django.views import View

# @method_decorator(csrf_protect, name='post') # 针对csrf_protect 第二种方式可以
# @method_decorator(csrf_exempt, name='post') # 针对csrf_exempt 第二种方式不可以
@method_decorator(csrf_exempt, name='dispatch')
class MyCsrfToken(View):
# @method_decorator(csrf_protect) # 针对csrf_protect 第三种方式可以
# @method_decorator(csrf_exempt) # 针对csrf_exempt 第三种方式可以
def dispatch(self, request, *args, **kwargs):
return super(MyCsrfToken, self).dispatch(request, *args, **kwargs)

def get(self,request):
return HttpResponse('get')

# @method_decorator(csrf_protect) # 针对csrf_protect 第一种方式可以
# @method_decorator(csrf_exempt) # 针对csrf_exempt 第一种方式不可以
def post(self,request):
return HttpResponse('post')

跨域请求

  • 对于前后端分离的报错:has been blocked by CORS policy: NO 'Access-Control-Allow-Origin' header is present on the reauested resource.

同源策略

  • 浏览器的同源策略:浏览器发现ip或端口是不一样的,就会认为存在风险,会进行拦截。
  • 推荐的方法是使用Nginx进行反向代理。

添加响应头解决跨域

  • 解决思路就是告诉浏览器:不进行拦截。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -> app01/middlewares/Mycors.py
from django.utils.deprecation import MiddlewareMixin

class MyCors(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"] = "DELETE, PUT, POST"
return response

# -> DjangoProject/settings.py
MIDDLEWARE = [
...
'app01.middlewares.Mycors.MyCors',
]

配置文件与反射

importlib模块

使用importlib,以字符串形式导入模块

1
2
3
4
5
6
7
from myfile import b
print(b)

import importlib
res = 'myfile.b'
ret = importlib.import_module(res) # 该方法最小只能到py文件名
print(ret)

实例展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import settings
import importlib

def send_all(content):
for path_str in settings.NOTIFY_LIST: # 'notify.email.Email'
module_path,class_name = path_str.rsplit('.', maxsplit=1)
# module_path = 'notify.email' class_name = 'Email'
# 1 利用字符串导入模块
module = importlib.import_module(module_path) # from notify import email
# 2 利用反射获取类名
cls = getattr(module, class_name) # Email、QQ、Wechat
# 3 生成类的对象
obj = cls()
# 4 利用鸭子类型直接调用send方法
obj.send(content)

Auth模块

使用auth模块要用就用全套

Django的admin路由对应的就是就是auth_user表,必须是管理员用户才能进入。

创建超级用户(管理员):python3 manage.py createsuperuser

常用方法

  • auth.authenticate(request, username, password):根据用户名在表中查询加密后的密码,并将请求中的密码加密比对是否正确。返回用户对象,不匹配则返回None
  • auth.login(request, user_obj):保存用户状态,建立session。执行该方法后,可以在任何地方通过request.user获取到当前登陆的用户对象
  • request.user
  • request.user.is_authenticated():判断当前用户是否登陆
  • @login_required:校验登录装饰器
    • 局部配置:@login_required(login_url='/login/')
    • 全局配置:settings.py -> LOGIN_URL = '/login/'
  • request.user.check_password(old_password):将请求中的密码加密,并比对原密码
  • request.user.set_password(new_password):修改用户对象的密码
  • request.user.save():将修改密码后的用户对象写入数据库
  • auth.logout(request) :注销,清除双方session
  • User.objects.create_user(username=username,password=password):注册用户。注意不能使用create方法,该方法不对密码进行加密。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
def login(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user_obj = auth.authenticate(request, username=username, password=password)
print(user_obj) # 用户对象,不存在则返回None
print(user_obj.username) # jason
print(user_obj.password) # pbkdf2_sha256$36000$zeNDf8CkZj7y$b+e/CjzZoAnbBIpvUWgz25ybBDqDzRTmYAHPytxqRYQ=
# 判断当前用户是否存在
if user_obj:
auth.login(request, user_obj) # 保持用户状态,类似于request.session[key] = user_obj
# 执行该方法后,可以在任何地方通过request.user获取到当前登陆的用户对象
return redirect('/home/')
return render(request, 'login.html')


@login_required(login_url='/login/')
def home(request):
print(request.user) # 在django_session中判断用户是否登陆,没有登录则为AnonymousUser匿名用户
print(request.user.is_authenticated())
return HttpResponse('home')


@login_required
def set_password(request):
if request.method == 'POST':
old_password = request.POST.get('old_password')
new_password = request.POST.get('new_password')
confirm_password = request.POST.get('confirm_password')
# 校验两次密码是否一致、老密码是否正确
if new_password == confirm_password and request.user.check_password(old_password):
request.user.set_password(new_password) # 仅仅是在修改对象的密码属性
request.user.save() # 这一步才是真正的操作数据库
return redirect('/login/')
return render(request, 'set_password.html', locals())


@login_required
def logout(request):
auth.logout(request) # 类似于request.session.flush()
return redirect('/login/')


def register(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
User.objects.create_user(username=username, password=password) # 创建普通用户
return render(request, 'register.html')

扩展auth_user表

  • 面向对象继承,自由增加字段,对象可以直接访问字段
  • 如果继承了AbstractUser,那么在执行数据库迁移命令的时候auth_user表就不会再创建出来了,而是替换成定义的子类
  • auth模块的功能还是照常使用
  • 前提:
    1. 在继承之前没有执行过数据库迁移命令、auth_user没有被创建
    2. 继承的类里面不要覆盖AbstractUser里面的字段名,只扩展额外字段
    3. 需要在配置文件中添加AUTH_USER_MODEL = 'app01.UserInfo'
1
2
class UserInfo(AbstractUser):
phone = models.BigIntegerField()

BBS实战

表设计

  1. 用户表(AbstractUser)

    • phone:电话号码
    • avatar:用户头像
    • create_time:创建时间
    • 一对一:个人站点表
  2. 个人站点表

    • site_name:站点名称
    • site_title:站点标题
    • site_theme:站点样式
  3. 文章标签表

    • name:标签名
    • 一对多:个人站点
  4. 文章分类表

    • name:分类名
    • 一对多:个人站点
  5. 文章表

    • title:文章标题

    • desc:文章简介

    • content:文章内容

    • create_time:发布时间

      数据库字段设计优化:虽然下述的三个字段可以从其他表里面跨表查询计算得出,但是频繁跨表效率

    • up_num:点赞数

    • down_num:点踩数

    • comment_num:评论数

    • 一对多个人站点

    • 多对多文章标签

    • 一对多文章分类

  6. 点赞点踩表(记录哪个用户给哪篇文章点了赞还是点了踩)

    • user:ForeignKey(to=”User”)
    • article:ForeignKey(to=”Article”)
    • is_up:BooleanField()
  7. 文章评论表

    • user:ForeignKey(to=”User”)

    • article:ForeignKey(to=”Article”)

    • content:CharField()

    • comment_time:DateField()

    • parent:ForeignKey(to=”self/Comment”,null=True)

      根评论子评论,评论当前发布的内容,根评论与子评论是一对多的关系