内容提要:
JWT用户认证原理和用户注册功能实现。
《Python前后端分离开发Vue+Django REST framework实战》作者bobby
——学习来源
7.2只对部分接口做权限校验在settings.py中配置了全局的认证后(REST_FRAMEWORK的DEFAULT_AUTHENTICATION_CLASSES),每个接口都会使用全局认证配置。
如果想只针对部分接口进行权限校验,则应该取消全局配置,在需要认证的接口中增加如下设置。
from rest_framework.authentication import TokenAuthentication
class GoodsListViewset(mixins.ListModelMixin,viewsets.GenericViewSet):
...省略...
authentication_classes = (TokenAuthentication,)
...省略...
7.3JWT用户认证原理
参考资料:https://www.cnblogs.com/wenqiangit/p/9592132.html
drf的token认证模式有缺点(例如token无过期),通常前后端分离项目采用JWT(Json Web Token)用户认证模式。
传统验证用户登录信息的方式,是由后端根据用户信息生成一个token,保存token和用户id存入数据库或session,把token传给用户,存入浏览器cookie,之后请求带上cookie,后端根据cookie来判断用户权限。缺点如下。
js可以操作cookie。
可以被xsrf。
验证信息如果存在数据库中,每次认证查询会加大开销。
Json Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以JSON对象的形式安全传递信息的方法。JWT可以使用HMAC算法或者是RSA的公钥秘钥进行签名。有以下优点。
简洁。
通过URL,POST参数或者HTTP header发送。
自包含。
负载包含了所有用户所需信息,避免了多次查询数据库。
JWT组成
Header头部:包含token类型和采用的加密算法。
Payload负载:存放用户的信息,例如iss(签发者)、exp(过期时间)等。
Signature签名:根据header的算法,对payload进行签名,生成后的秘钥串会和header和payload放在一起。(服务器判断时会对秘钥串进行解密或者对前面的base进行加密比对,如果不同则表示token是伪造的)
JWT是通过算法进行用户信息的获取,不需要存储到服务器。
JWT用户认证流程
7.4django-rest-framework使用JWT使用github上第三方库djangorestframework-jwt实现DRF的JWT。
安装djangorestframework-jwt。
pip install djagnorestframework-jwt -i https://pypi.douban.com/simple
在setting.py中增加jwt配置。
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 2,
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_jwt.authentication.JSONWebTokenAuthentication', # 取出用户信息放入request.user
],
}增加urls配置。
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
url(r'^jwt-auth/', obtain_jwt_token),
]获取toknen后,放入header即可。
{'Authorization': 'JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1
c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTk2OTQ2MDA1LCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSJ9
.uQzIc4Bonar5faWQThKmSTMcDnitiIW0uWh6ziB6tLg'}
其他的常用配置如下(github对应的文档打不开了):
# settings.py
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA':datetime.timedelta(seconds=300), # token有效期
# 'JWT_EXPIRATION_DELTA':datetime.timedelta(days=7),
'JWT_AUTH_HEADER_PREFIX': 'JWT', # 设置header请求时Authorization值的前缀
}
7.5自定义用户认证函数
默认的用户认证(authenticate)使用的是用户名密码验证,如果想自定义方式验证(例如输入的username可以匹配手机号),可以自定义编写一个继承ModelBackend,重写authenticate方法的类。
# users的views.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q
User = get_user_model()
class CustomBackend(ModelBackend):
"""自定义用户认证"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username)|Q(mobile=username))
if user.check_password(password):
return user
except Exception as e:
return None
在settings.py中配置authentication_backends属性,指定自定义类为认证时调用的类。
# settings.py
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
)
7.6通过serializers单独验证models某个字段
在之前的serializers中,直接使用了serializers.ModelSerializer,但是验证码模块不适合这样使用。因为验证码模块的mobile和code都是必填,如果用ModelSerializer则会校验这两项(但我们只需求他mobile是必填),所以我们可以改为使用serializers.Serializer。
在serializers.Serializer里,如果想针对某个字段做自定义的校验,可以重写validate_开头的函数。
import re
from datetime import datetime,timedelta
from rest_framework import serializers
from django.contrib.auth import get_user_model
from DRFDemo.settings import REGEX_MOBILE
from .models import VerifyCode
User = get_user_model()
class GoodsCategorySerializer3(serializers.Serializer):
mobile = serializers.CharField(max_length=11)
def validate_mobile(self,mobile):
if User.objects.fileter(mobile = mobile).count():
raise serializers.ValidationError("用户已经存在")
# settings.py配置的属性REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"
if not re.match(REGEX_MOBILE,mobile):
raise serializers.ValidationError("手机号码非法")
# 验证码发送频率
one_mintes_ago = datetime.now() - timedelta(hours=0,minutes=1,seconds=0) # 前一分钟的时间
if VerifyCode.objects.filter(add_time__gt=one_mintes_ago,mobile=mobile): # 搜索该手机号是否有1分钟内发送验证码的记录
raise serializers.ValidationError("发送验证码间隔小于60秒")
return mobile
在views.py中的serializer验证和froms验证的方式是一样的。
from .serializers import VerifyCode
from .serializers import SmsSerializer
from rest_framework.mixins import CreateModelMixin
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
class SmsCodeViewset(CreateModelMixin,viewsets.GenericViewSet):
"""发送短信验证码"""
serializer_class = SmsSerializer
# 重写create
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) # 如果执行抛异常则不往后执行,返回400状态码
mobile = serializer.validated_data["mobile"]
# 此处就可以编写创建逻辑代码,此处省略。下面为简略实现验证通过后的新增
code_record = VerifyCode(code=1234,mobile=mobile)
code_record.save()
return Response({
"status":0,
"mobile":mobile
},status=status.HTTP_201_CREATED)
7.7serializers字段验证
除了自定义的validate_xxx之外,DRF还提供了validation class,用于提供便捷的字段的验证。
官方文档:https://www.django-rest-framework.org/api-guide/validators/
例如:字段是唯一,如果数据库已经存在则返回错误。
# serializers.py
from rest_framework.validator import UniqueValidator
username = serializers.CharField(required=True,allow_blank=False,validators=[UniqueValidator(queryset=User.objects.all(),message="用户已经存在")])
# message是错误时返回的提示内容
用户注册时,如果没有做特殊处理,保存的密码也是明文的,这样会导致安全问题。如果想设置把密码加密后保存,可以采用以下两种方法。
方法一:重写create方法
重写create方法,将password加密后在存储。
# serializers.py
from rest_framework import serializers
class UserRegSerializer(serializers.ModelSerializer):
def create(self,validated_data):
user = super(UserRegSerializer, self).create(validated_data=validated_date)
user.set_password(validated_date["password"])
user.save()
return user
方法二:使用信号量。
信号量是指django进行某些操作后(例如model),会发送一个全局信号,该信号可以捕捉,捕捉后加入自己的逻辑处理。
官方文档:https://docs.djangoproject.com/en/2.1/ref/signals/#django.db.models.signals.post_save
用户保存后会发送post_save信号,通过捕获该信号来控制密码保存操作。
新建signals.py文件。
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
User = get_user_model()
@receiver(post_save,sender=User) # 指定接收信号,从哪个model传递过来的。
def create_auth_password(sender,instance=None,created=False,**kwargs):
# instance代表models的对象,create代表该信号是否为新建操作
if created:
password = instance.password
instance.set_password(password)
instance.save()在apps.py中重写ready函数。
from django.apps import AppConfig
class UserConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals
注册后可以让用户输入用户名密码自行登录,也可以直接当做用户已登录(返回首页)。如果需要注册成功后默认是登录状态,则需要在注册成功的接口中返回token(JWT模式)。
from rest_framework import viewsets
from rest_framework.mixins import CreateModelMixin
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import get_user_model
from .serializers import UserRegSerializer
User = get_user_model()
class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
"""用户注册"""
serializer_class = UserRegSerializer
queryset = User.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 修改下面的内容
user = self.perform_create(serializer)
# 以下代码需要自行断点和查阅源码后编写,找到jwt如何生成token
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
headers = self.get_success_headers(serializer.data)
# 返回时使用更新后的数据re_dict
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
return serializer.save() # 需要返回user对象
7.10serializers的常用知识
self.initial_data:存储了前端传递过来的参数,例如self.initial_data["username"]获取前端传参username。
def validate(self,attrs):作用域所有的serializers之上,attrs是所有字段validate_xx处理之后,dict类型的所有参数。用于对所有参数做最后的处理。
def validate(self,attrs):
# 将username赋值给mobile
attrs["mobile"] = attrs["username"]
# 删除多余字段code
del attrs["code"]
return attrshelp_text:文档的提示内容。
code = serializers.CharField(required=True,max_length=4,min_length=4,help_text="验证码")
error_messages:对应字段错误时的提示信息。
code = serializers.CharField(required=True,max_length=4,min_length=4,error_messages={"blank":"请输入验证码",required":"请输入验证码","max_length":"验证码格式错误","min_length":"验证码格式错误"},help_text="验证码")
write_only:返回前端时不序列化该字段。
code = serializers.CharField(required=True,write_only=True,max_length=4,min_length=4,error_messages={"blank":"请输入验证码",required":"请输入验证码","max_length":"验证码格式错误","min_length":"验证码格式错误"},help_text="验证码")