介绍我在使用django框架中用到几个非常便捷的小功能,包括
- django-restframework
- 身份认证类authentication_classes
- 权限检查类permissoin_classes
- 自带的日志收集LOGGING,
- 中间件middleware
django-restframework
REST框架是用于构建Web API的功能强大且灵活的工具包,在官方文档中已经介绍了如何安装和配置,以及这篇博客也介绍的比较详细,引用博客中的一段:
使用基于类的视图的一大优势在于它允许我们轻松地编写可重用的代码。到目前为止,我们一直使用的增删改查等操作,对于我们创建的任何模型支持的API视图都非常相似的。这些常见行为在REST框架的Mixin是类中实现起来就非常简单。Mixins
类中重写了增删改查查五个最常见的视图类的处理方法,根据我们的需要,我们可以选择性的继承其中的处理方法,从而只关系我们需要什么,就继承什么了。
mixins.CreateModelMixin
mixins.mixins.ListModelMixin
mixins.RetrieveModelMixin
mixins.UpdateModelMixin
mixins.DestroyModelMixin
"""
Basic building blocks for generic class based views.
We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
"""
from rest_framework import status
from rest_framework.settings import api_settings
from rest_framework.response import Response
class JSONResponse(Response):
def __init__(self, data=None, status=None,
template_name=None, headers=None,
exception=False, content_type=None,
code=0, msg="success"):
super().__init__(data, status, template_name, headers, exception, content_type)
self.data = {"code": code, "msg": msg, "data": data}
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return JSONResponse(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
class ListModelMixin:
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return JSONResponse(serializer.data)
class RetrieveModelMixin:
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return JSONResponse(serializer.data)
class UpdateModelMixin:
"""
Update a model instance.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return JSONResponse(serializer.data)
def perform_update(self, serializer):
serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
class DestroyModelMixin:
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return JSONResponse(status=status.HTTP_200_OK)
def perform_destroy(self, instance):
instance.delete()
接着 你就可以根据需求(增删改查)不同,继承不同的类,以此定义你所需要的Model类,列举示例:
#增删改查model
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
#只读model
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `list()` and `retrieve()` actions.
"""
pass
在views中进行使用示例,只需继承你所定义的model类,就已经实现了增删改查的接口,也可以进行重写,示例:
class TaskViewSet(ModelViewSet):
serializer_class = TaskSerializer
pagination_class = JSONPagination
permission_classes = (IsAuthenticated,)
authentication_classes = (BasicAuthentication,
JWTAuthentication,
TokenAuthentication,
SessionAuthentication,
)
search_fields = ('id', 'uid', "status", "user__username") # foreign_key__related_fieldname
filter_fields = ('status', 'id')
进行路由注册:
#urls.py中添加
router.register(r'task', SubmitTaskViewSet, 'task')
既然增删改查接口都已经实现了,那么前端如果对这些接口进行调用呢,进行粗略的总结如下:
方法 | URL示例 | |
增 | post | http://localhost:8000/api/v1/task/ |
删 | delete | http://localhost:8000/api/v1/task/1/ |
改 | patch | http://localhost:8000/api/v1/task/1/ |
列表查询 | get | http://localhost:8000/api/v1/task/ |
单个查询 | get | http://localhost:8000/api/v1/task/1/ |
指定单条记录进行删改时,后跟主键,在本例中是ID,而在重写方法中,可通过pk参数获取这个ID,例如:
class TaskViewSet(ModelViewSet):
def update(self, request, *args, **kwargs):
status = data.get('status', None)
pk = self.kwargs.get('pk', '')
res = task.objects.filter(id=pk).update(status=status)
return res
小小坑点:在viewset中 返回对象必须是model的QuerySet类型,需要注意。
身份认证类authentication_classes/权限检查类permissoin_classes
这两个类都来自于rest_framwork,我在最初总是将二者搞混,分不清具体作用,
在访问接口时需要进行认证(包括用户名密码、session、token等认证方法),authentication_classes,就是用于用户的身份校验
- BasicAuthentication,基于用户名密码认证方式
- SessionAuthentication,基于Session认证方式
- TokenAuthentication,基于令牌认证方式(一个用户绑定一个令牌,每次请求在请求头中加上
也可再继承自定义身份认证的类,用于判断该用户是否有操作这个接口的权限,例如我的服务提供给T服务的后端进行调用,我需要给T提供一个固定的token,可以这样写:
首先生成一个token,为T服务创建一个用户
class TAuthentication(TokenAuthentication):
t_token = 'fds87fd4f67fda2wrfd5f'
# token 手动插入到authotoken_token表中
def authenticate(self, request):
if 'HTTP_AUTHORIZATION' not in request.META and \
request.META.get("HTTP_SERVER_TOKEN") == self.t_token:
user = User.objects.get(username=t_server)
return user, None
else:
return super(SocAuthentication, self).authenticate(request)
在调用时,头部中加入"Server-Token":"fds87fd4f67fda2wrfd5f",即可进行认证。
在身份认证之后,知道你已经是我们库的用户,但是不一定每一个用户都有调用此接口的权限,权限检查类permissoin_classes,用于控制接口的操作权限,权限可通过修改auth_user/auth_group/auth_user_groups/auth_group_permissions/auth_permission等多个表,达到控制多种权限的目的。rest已经提供以下几个基本的权限控制类:
# 来自rest_framework/permissions.py
class AllowAny(BasePermission):
"""
Allow any access.
This isn't strictly required, since you could use an empty
permission_classes list, but it's useful because it makes the intention
more explicit.
"""
def has_permission(self, request, view):
return True
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
class IsAdminUser(BasePermission):
"""
Allows access only to admin users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_staff)
假如我现在这个接口只希望某个用户组内的成员进行访问,可以这样进行定义
class TaskPermission(permissions.IsAuthenticated):
def has_permission(self, request, view):
if request.user.username == "admin":
return True
task_group = Group.objects.get(name='task.group').user_set.all()
if request.user in task_group:
return True
return False
自带的日志收集LOGGING
参考了这篇博客,文章中已经记录的很全,我就不做赘述,这个对于我这种懒人真的好方便,可以通过LOGGING定义日志的格式,记录错误、警告等,还可以记录数据库操作日志,将日志输出到文件,比如定义:
# 部分代码
LOGGING = {
'handlers': {
'file': {
'class': 'logging.FileHandler',
'filename': '/home/work/log/db_operate.log', #这是将普通日志写入到日志文件中的方法,
'formatter': 'db'
},
},
"loggers": {
"django.db.backends": {
"handlers": ["file"],
"propagate": True,
"level": "DEBUG",
},
}
}
记录的日志文件格式如下所示:
[DEBUG django.db.backends] 2021-05-26 10:50:29 +0800 :
(0.003) SELECT `django_migrations`.`id`, `django_migrations`.`app`, `django_migrations`.`name`, `django_migrations`.`applied` FROM `django_migrations`; args=()
我说下项目中最后还是没有用这个方法的原因,
1、这种方式会记录全部数据库操作,包括增删改查,而部分表的查询操作太过频繁,导致日志文件占用内存爆炸
2、不方便进行日志的查询,项目启动时加载setting,不方便修改日志的文件名,从而导致无法将日志按日期记录到多个文件
3、针对的也是所有的表,这种即完善又不便,不能只记录部分表的操作(我还试图使用cron去重定向日志文件名)
4、无法与接口调用相结合,换言之 无法知道是哪位用户的操作这条记录。
以上种种不便,我开始使用中间件记录,两种方法也许并不能混为一谈,毕竟中间件是针对接口调用进行记录的,很多在后端使用cron方法操作的数据库日志,无法体现。而且日志中也不是直接对sql语句进行记录,而是接口请求的request、response等。
我还是根据项目需求 选择了使用中间件进行记录。通过记录request的user/body/path等,可以知道什么时间点,谁调用了哪个接口,接口是我写的呀,从而我就知道这个用户进行了什么操作。
中间件middleware
中间件是在接口到达你定义的view方法之前,已经方法返回response给用户之前做一些处理的工具,参考了这篇博客1,博客2引入文中以下内容:
传统方式自定义中间件其实就是在编写五大钩子函数:
- process_request(self,request)
- process_response(self, request, response)
- process_view(self, request, view_func, view_args, view_kwargs)
- process_exception(self, request, exception)
- process_template_response(self,request,response)
可以实现其中的任意一个或多个!
钩子函数 | 执行时机 | 执行顺序 | 返回值 |
---|---|---|---|
process_request | 请求刚到来,执行视图之前 | 配置列表的正序 | None或者HttpResponse对象 |
process_response | 视图执行完毕,返回响应时 | 逆序 | HttpResponse对象 |
process_view | process_request之后,路由转发到视图,执行视图之前 | 正序 | None或者HttpResponse对象 |
process_exception | 视图执行中发生异常时 | 逆序 | None或者HttpResponse对象 |
process_template_response | 视图刚执行完毕,process_response之前 | 逆序 | 实现了render方法的响应对象 |
process_request(request)
只有一个参数,也就是request请求内容,和视图函数中的request是一样的。所有的中间件都是同样的request,不会发生变化。它的返回值可以是None也可以是HttpResponse对象。返回None的话,表示一切正常,继续走流程,交给下一个中间件处理。返回HttpResponse对象,则发生短路,不继续执行后面的中间件,也不执行视图函数,而将响应内容返回给浏览器。
process_response(request, response)
有两个参数,request和response。request是请求内容,response是视图函数返回的HttpResponse对象。该方法的返回值必须是一个HttpResponse对象,不能是None。
process_response()方法在视图函数执行完毕之后执行,并且按配置顺序的逆序执行。
下面介绍我自定义的中间件,实现对部分接口进行定义,同时记录request和response
from django.utils.deprecation import MiddlewareMixin
import datetime
import json
class DBLogMiddleware(MiddlewareMixin):
_initial_http_body = None
def process_request(self, request):
self._initial_http_body = request.body
def process_response(self, request, response):
if request.method == 'GET':
return response
task_router = ['/api/v1/task', ]
db_log_file = "/home/work/log/submit-operate-" + datetime.date.today().strftime('%y-%m-%d')
for router in task_router:
if router in request.path:
with open(db_log_file, "w+") as f:
content = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "----" + request.user.username + \
"----" + request.method + "----" + request.path + "----" + str(self._initial_http_body) + "\n\n"
f.write(content)
pass
return response
小坑点:在process_request中,因为是在请求刚到来的时候,还未进行用户的身份认证,所以无法从request.user中得知用户的身份,
小坑点2:在process_response中,试图修改request.body出错,You cannot access body after reading from request‘s data stream,因为此时body已经设为不可读,所以我将body在request到达时就赋予一个私有成员,来达到修改和访问的目的。
django就介绍这么多,项目也结束了,如果以后再发现什么新功能,会进行补充~
感谢前几篇博客为我点赞的大佬,本麻瓜备受鼓舞~