以前看到有很多人提问有关Django定制User的问题,正好教程准备讲到REST
与OAuth
,那么在这里就先说一下有关REST framework
,这里就讲一下有关定制User模型以及REST framework
验证受权相关的问题,不过在后续教程的实际应用中仍是采用第三方登陆的方式作验证受权,事实上在个人博客上已经不打算作用户功能了(感受仍是在QQ群交流比较好吧)。对这部分不感兴趣的能够直接跳过了。前端
Django原生的User模型已经足够知足通常小网站的需求,可是有时候不可避免要对用户模型作一些定制,官方文档给出了四种方法:python
前两个方法适用于只要扩展用户信息或增长一些处理方法而和身份验证无关,然后二者则适用于对于身份验证有定制需求。sql
Django官方文档对于如何定制用户模型有着详尽的解释,这里仅仅讲讲我在某次实践中是如何使用的。shell
首先咱们能够新建一个Django app
,咱们能够把验证受权相关的功能都放在这里,假定命名为core
。假如咱们须要多种分级的等级标识,而不只仅是原生User
模型的is_staff
字段指示用户是不是管理员,例如须要三重等级,能够像下面这样编写代码:数据库
# core/models.py class User(AbstractUser): Level_Set = ( (0, 'Super User'), (1, 'Normal User'), (2, 'Internship'), ) level = models.IntegerField(choices=Level_Set, default=2) class Meta: ordering = ('date_joined',)
以后须要在项目的settings.py
文件中加入:django
# 字符串内容是“app名.模型名” AUTH_USER_MODEL = 'core.User'
能够在core/admin.py
中注册咱们的定制模型:后端
from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import User admin.site.register(User, UserAdmin)
有一点须要注意,Django的模型须要迁移操做,对于定制的User
,最好在项目刚刚开始的时候,在你尚未执行第一次python manage.py migrate
的时候完成上述操做,固然若是还在开发阶段,即便以前执行过迁移操做,也能够经过删除项目中全部migrations
文件夹以及sqlite
文件来初始化。api
此后,若是你的模型中有须要自定义用户模型作外键的需求,例如文章与文章做者,能够参考以下设置:浏览器
from django.conf import settings from django.db import models class Article(models.Model): author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, )
任何须要使用到咱们自定义用户模型的地方均可以这样操做。安全
能够参考以下代码:
# core/serializers.py from rest_framework import serializers from core.models import User class UserSerializer(serializers.HyperlinkedModelSerializer): password = serializers.CharField(style={'input_type': 'password'}, label='密码', write_only=True) class Meta: model = User fields = ['url', 'id', 'username', 'password', 'email', 'level', 'is_active', 'date_joined']
这里咱们设置password
字段时加入了write_only=True
这个参数,这样咱们的view视图将只会在处理POST
、PUT
、PATCH
请求时(若是你容许这些请求的话)写入密码而不会在返回用户列表或详情信息时显示密码。
接下来能够写个简单的视图试试:
# 别忘了引入咱们自定义的模型与序列化器 class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.get_queryset() serializer_class = UserSerializer
还记得怎么在urls.py
经过router
注册视图吗?可是若是你使用了多个app
,那么在不一样app
中注册会产生冲突,一个解决办法是后端只使用一个app
而不是不一样功能拆分到不一样app,或者能够作以下尝试:
# app1的urls.py from . import views routeList = ( (r'users', views.UserViewSet), ) # app2的urls.py from . import views routeList = ( (r'articles', views.ArticleViewSet), ...... ) # 项目级urls.py from app1.urls import routeList as app1Urls from container.urls import routeList as app2Urls routeList = app1Urls + app2Urls router = DefaultRouter() for route in routeList: router.register(route[0], route[1]) urlpatterns = [ path('', include(router.urls)), path('api-auth/', include('rest_framework.urls')) ....... ]
如今尝试使用POST
请求建立一个新用户吧,最简单的方法是直接用浏览器打开访问127.0.0.1:8000/users/。接着使用新建的帐户密码验证登陆,你会发现验证失败。
为了安全起见,咱们设置的密码会通过加密处理再放入数据库,一样,验证用户密码时,也会对密码加密再比对密文,这样即便是拥有查看数据库权限的人也没法查看用户密码的明文。可是这里咱们的视图没有对密码进行加密就被存入了数据库,而用户验证时倒是用的Django自身的API,比对的是密文,也就是验证时你提交的密码被加密,而数据库中的密码却没有加密,这样就出现了没法匹配的现象。
能够经过覆写ViewSet
的create
方法来修复这个bug:
from django.contrib.auth.hashers import make_password ...... class UserViewSet(viewsets.ModelViewSet): ...... def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.validated_data['password'] = make_password(serializer.validated_data['password']) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
这里调用Django提供的make_password
函数来生成正确的加密的密码。
既然是编写REST
风格的API,那么建议对于用户的增长、修改、删除都使用这个视图。对于用户改密码的需求,能够在序列化器中添加一个old_password
字段,并设置为当前密码,同时要改写视图类的partial_update
方法。如下是一个我用来实现超管直接修改全部用户密码的需求(不要问我为何会有这种需求~)的方式:
class UserViewSet(viewsets.ModelViewSet): ...... def partial_update(self, request, *args, **kwargs): if 'password' in request.data: request.data['password'] = make_password(request.data['password']) kwargs['partial'] = True return self.update(request, *args, **kwargs)
经过设置partial
参数为True
并将内容传递给update
来实现仅针对密码部分更新。
常规状况下咱们经过用户的用户名与密码来识别用户身份,最基础的方法是每次请求都须要用户名及密码,可是这极有可能暴露敏感信息,通常不采用。比较常见的方式是基于OAuth
、Session
以及Token
的验证方式。REST framework
为咱们提供了可用的TokenAPI,这里介绍一下在此基础上作一些扩展。固然通常状况下,其实有着开箱可用的第三方库,如django-rest-knox
,可是在学习时咱们能够重复造点轮子来加深理解。
简单的说,基于Token的验证就是客户端发送用户密码,服务端建立一个与用户相对应的随机字符串,以后客户端每次请求时在请求头中加上这段字符串,便可经过验证。
为了使用REST framework
提供的Token咱们须要在settings.py
中注册:
INSTALLED_APPS = [ ... 'rest_framework.authtoken' ]
若是你已经建立过用户,可使用命令python manage.py shell
,按以下操做:
>>> from core.models import User >>> from rest_framework.authtoken.models import Token >>> for user in User.objects.all(): >>> Token.objects.get_or_create(user=user)
同时修改core/models.py
,经过Django的信号机制,在每次新建用户时为其建立Token:
...... @receiver(post_save, sender=settings.AUTH_USER_MODEL) def create_auth_token(sender, instance=None, created=False, **kwargs): # 接收用户建立信号,每次新建用户后自动建立token if created: Token.objects.create(user=instance)
接下来修改你须要添加权限的视图:
from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated ...... class ArticleViewSet(viewsets.ModelViewSet): authentication_classes = [TokenAuthentication] permission_classes = [permissions.IsAuthenticated] queryset = Article.objects.all() serializer_class = ArticleSerializer
经过authentication_classes
指定要使用的验证类,有关permission_classes
的内容下节在说。如今咱们设置一下项目的urls.py
:
from rest_framework.authtoken import views urlpatterns = [ ...... path('api-token-auth/', views.obtain_auth_token), ]
如今向该接口发送POST请求提交用户密码,将会获得Token,仅在将该Token放在请求头headers
中,才可获得articles
的正确响应,使用命令行工具httpie调试的示例以下:
$ http POST http://127.0.0.1:8000/api-token-auth/ username="user" password="password" HTTP/1.1 200 OK ...... { "token": "bed522b6f41b962b5c829598e990b9f058518c9d" } $ http http://127.0.0.1:8000/articles/ 'Authorization: Token bed522b6f41b962b5c829598e990b9f058518c9d'
你能够尝试一下不带Authorization
这一串会获得什么响应。
可是REST framework
自带的Token有着不小的缺陷,最典型的一点是这个Token没有过时机制,这意味着若是有谁截获了你的Token,就能够无限制的使用,安全风险实在太大。下面咱们来试试扩展一下原生的Token验证,新建core/authentication.py
:
import datetime from django.conf import settings from django.core.cache import cache from rest_framework.authentication import TokenAuthentication from rest_framework import exceptions from django.utils.translation import ugettext_lazy as _ # 记得要在settings.py中设置REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES变量 # 这是为了方便之后调节过时时间,例如给该变量赋值为60,则为一小时过时 EXPIRE_MINUTES = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES', 1) class ExpiringTokenAuthentication(TokenAuthentication): """ Setup token expired time """ def authenticate_credentials(self, key): model = self.get_model() # 利用Django的cache减小数据库操做 cache_user = cache.get(key) if cache_user: return cache_user, key try: token = model.objects.select_related('user').get(key=key) except model.DoesNotExist: raise exceptions.AuthenticationFailed(_("无效令牌")) if not token.user.is_active: raise exceptions.AuthenticationFailed(_("用户被禁用")) time_now = datetime.datetime.now() if token.created < time_now - datetime.timedelta(minutes=EXPIRE_MINUTES): token.delete() raise exceptions.AuthenticationFailed(_("认证信息已过时")) if token: # EXPIRE_MINUTES * 60 because the param is seconds cache.set(key, token.user, EXPIRE_MINUTES * 60) return token.user, token
同时咱们能够修改core/views.py
,定制验证视图,若是当前Token没有过时则返回cache中的Token,不然建立新Token:
from rest_framework.authtoken.views import ObtainAuthToken ...... class ObtainExpiringAuthToken(ObtainAuthToken): # 别忘了from rest_framework.authentication import BasicAuthentication # 这是经过post用户名密码获取token的视图,可不能采起token验证哦 authentication_classes = [BasicAuthentication] def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): user = serializer.validated_data['user'] token, created = Token.objects.get_or_create(user=user) time_now = datetime.datetime.now() if created or (token.created < time_now - datetime.timedelta(minutes=EXPIRE_MINUTES)): token.delete() token = Token.objects.create(user=user) token.created = time_now token.save() # 这里能够定制返回信息 context = { 'id': user.id, 'username': user.username, 'token': token.key } return Response(context) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
这样咱们要修改urls.py
以启用咱们新的验证视图:
from core.views import ObtainExpiringAuthToken urlpatterns = [ ...... path('api-token-auth/', ObtainExpiringAuthToken.as_view()), ]
如今你能够修改settings.py
中的REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES变量为1
来看看Token过时的效果。
既然有了验证,也就是对用户的身份进行识别是管理员、普通用户,仍是未登陆用户,那么确定要针对不一样类型的用户给予不一样权限,不然整个验证过程就失去了意义。事实上咱们以前在articles
API中已经使用了REST framework
提供的IsAuthenticated
权限,指定只有通过登陆验证的用户能够访问。如今让咱们设置一个基于用户级别的权限吧,新建core/permissions.py
:
from rest_framework import permissions class AdministratorLevel(permissions.BasePermission): # 客户端向服务端发送请求后,此方法被调用,根据返回的布尔值决定用户是否拥有权限 def has_permission(self, request, view): if request.user.is_authenticated: if request.method in permissions.SAFE_METHODS: return True # 普通管理员可修改数据 elif request.method.upper() in ('POST', 'PUT', 'PATCH') and request.user.level == 1: return True # 超级管理员拥有全部权限 elif request.user.level == 0: return True else: return False return False
如今能够修改articles API
的视图,用咱们自定义的权限类替换掉以前的IsAuthenticated
,而且新建多个不一样等级的用户,试试它们的权限吧。
顾名思义,throttling起到节流做用,它和permissions有些相似,但能够用来限制客户端的请求频率。
例如,咱们想要用户的一个Token在一小时内过时,但只要用户保持活跃,那么在较长的一段时间内没必要重复登陆。能够添加一个经过旧Token获取新Token的接口,由前端判断若是用户在活跃状态下,那么能够在用户不知道的状况下获取新的Token。
# core/views.py from rest_framework.views import APIView ...... class TokenForToken(APIView): authentication_classes = [ExpiringTokenAuthentication] permission_classes = [permissions.IsAuthenticated] def get(self, request, format=None): user = request.user # 这里有个小bug,留给读者去思考了 token, created = Token.objects.get_or_create(user=user) time_now = datetime.datetime.now() token.delete() token = Token.objects.create(user=user) token.created = time_now token.save() return Response({'token': token.key}
在urls.py
中注册此视图,咱们就能够用旧的Token来替换新的Token,可是若是你想要限制用户使用此方法的次数,则能够设置Throttling
。以下修改settings.py
:
REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.UserRateThrottle' ], 'DEFAULT_THROTTLE_RATES': { 'user': '10/day' } }
接着在core/views.py
中修改:
from rest_framework.throttling import UserRateThrottle ...... class TokenForToken(APIView): authentication_classes = [ExpiringTokenAuthentication] permission_classes = [permissions.IsAuthenticated] throttle_classes = [UserRateThrottle] ......
这样能够限制每一个用户天天最多请求10次。更多throttling的用法请查看REST framework
官方文档。
欢迎关注个人公众号“公子政的宅平常”,原创技术文章第一时间推送。