drf-JWT认证

一 jwt介绍和原理

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

1.1 cookie,session,token发展历史

# https://www.cnblogs.com/liuqingzheng/p/16154439.html

# cookie,session,token发展历史
    -会话管理
    -cookie:客户端浏览器的键值对
    -session:服务的的键值对(djangosession表,内存中,文件,缓存数据库)
    -token:服务的生成的加密字符串,如果存在客户端浏览器上,就叫cookie
    	-三部分:头,荷载,签名
        -签发:登录成功,签发
        -认证:认证类中认证
        
# 不正常访问:
    -篡改:把头跟荷载放在一起加密跟签名比较,一致,就说明是原来的用户信息,没有被篡改
    -伪造:怎么防止伪造,你不知道我的密钥和使用哪种加密方式
    
    
# jwt:Json web token (JWT),web方向的token认证。token包含了(jwt)
    -典型的jwt串,长得样子:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

1.2 认证过程


加锁:可能有并发情况,把写操作加锁,保证数据对应。


集群

  • 同样的项目部署在多台服务器上。
  • 前端的访问地址都是nginx地址,nginx做请求转发。
  • 使用集群服务器的目的是:1.提高并发量,2.防止一台服务器崩掉。

1.3 JWT的构成

JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature).

1.3.1 header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
  • 公司信息

头部的内容如下:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64编码,构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

1.3.2 payload

载荷就是存放有效信息的地方。

  • 当前用户信息,用户名
  • 当前用户信息,用户id
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间

定义一个payload:

{
  "exp": "1234567890",  # 时间戳
  "name": "John Doe",
  "user_id":99
}

然后将其进行base64编码,得到JWT的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

1.3.3 signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret 密钥

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret,组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

二 base64编码和解码

# base64并不是一种加密反射,只是编码解码方式

# 字符串,可以转成base64编码格式:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 解码成base64

2.1 base64使用

import json
import base64  # 内置模块

d = {'user_id': 1, 'username': "lqz"}

d_str = json.dumps(d)
print(d_str)
# 对字符串进行bashe64 编码,必须是二进制格式
res = base64.b64encode(bytes(d_str, encoding='utf-8'))
print(res)  # eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImxxeiJ9

# 解码
res2 = base64.b64decode('TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=')
print(res2)

# 记住: base64 编码,长度一定是4的倍数。如果不够,用 = 补齐

2.2 base64的用途

  1. 互联网中,前后端数据交互,使用base64编码。举例:百度中密码是加密后又转成base64编码方式
  2. jwt 字符串使用base64编码。
  3. 互联网中一些图片,使用base64编码。举例:12306扫码登陆的二维码图片就是使用base64编码。
s = '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'
res = base64.b64decode(s)
with open('code.png', 'wb') as f:
    f.write(res)

三 jwt开发流程

# 使用jwt认证的开发流程,就是两部分
    -第一部分:签发token的过程,登录做的
    	-用户携带用户名和密码,访问我,我们校验通过,生成token串,返回给前端
        # 签发token有封装好的,可以直接使用。也可以自己写。
    -第二部分:token认证过程,登录认证时使用,其实就是咱们之前讲的认证类,在认证类中完成对token的认证操作
    	-用户访问我们需要登陆后才能访问的接口,必须携带我们签发的token串(请求头)
        -我们取出token,验证该token是否过期,是否被篡改,是否是伪造的
        -如果正常,说明荷载中的数据,就是安全的,可以根据荷载中的用户id,查询出当前登录用户,放到request中即可
        # 怎么把当前用户放到request中
        方式一:使用drf,写一个认证类,继承BaseAuthentication,重写authenticate方法中直接返回两个值,第一个值就是user(当前用户)
        方式二:使用中间件保存user
        只要是在执行视图类之前完成token认证过程就行。

四 drf-jwt快速使用

补充:

# 补充:
    - auth中的密码:密码明文一样,每次加密后都不一样,如何做,动态加盐,动态的盐要保存,跟密码保存在一起
    sha256加密方式$过期时间(时间戳形式)$动态盐$真正的密码数据,最后使用base64编码
        -加密函数:make_password
        -校验密码函数:check_password
    
    - 有代码,有数据库,没有登录不进去的系统
    
# 解决不了:
    token被获取,模拟发送请求
        不能篡改
        不能伪造

# djagno+drf框架中,使用jwt来做登录认证

# 使用第三方:
    -django-rest-framework-jwt:https://github.com/jpadilla/django-rest-framework-jwt
    -djangorestframework-simplejwt:https://github.com/jazzband/djangorestframework-simplejwt
    
# 我们可以自己封装 :https://gitee.com/liuqingzheng/rbac_manager/tree/master/libs/lqz_jwt

我们以djangorestframework-jwt为例讲解使用过程。

# 安装
    pip3 install djangorestframework-jwt
    
    
    
# 快速使用
    -签发过程(快速签发),必须是auth的user表(人家帮你写好了),只能使用用户名和密码登录
    	-登录接口--> 基于auth的user表签发的
    -认证过程
    	-登录认证,认证类(写好了)
        
        
        
# 总结:
-签发:只需要在路由中配置
    1.签发是基于auth中的user表的,先执行迁移记录
    2.增加数据:创建超级用户
    3.配置路由
        from rest_framework_jwt.views import obtain_jwt_token
        urlpatterns = [
            path('login/', obtain_jwt_token),   # 登录接口就有了,并且可以签发token,如果使用auth的user表做登录,不需要写额外的代码了
        ]
        

-认证:
    -1.视图类上加
        # 必须登录后才能访问
        from rest_framework_jwt.authentication import JSONWebTokenAuthentication
        from rest_framework.permissions import IsAuthenticated  # 是否认证通过

        class BookView(APIView):
            authentication_classes = [JSONWebTokenAuthentication] # 认证类,drf-jwt提供的
            permission_classes = [IsAuthenticated] # 权限类,drf提供的
            
    -2.访问的时候,要在请求头中携带,必须叫
        Authorization:jwt token串
            
            
# 显示中文错误信息
注册两个应用:rest_framework,rest_famework_jwt
settings中把语言改成中文'zh_hans'

五 drf-jwt定制返回格式

# 登录签发token的接口,要返回code,msg,username,token等信息

# 对返回格式进行定制
    -1 写个函数,函数返回字典格式,返回的格式,会被序列化,前端看到	
    	def common_response(token, user=None, request=None):  # 参数要记一下
            return {
                'code': '100',
                'msg': '登录成功',
                'username': user.username,
                'token': token,
            }

    -2 写的函数配置一下
    # djangorestframework-jwt的配置
        JWT_AUTH = {
        'JWT_RESPONSE_PAYLOAD_HANDLER':'app01.jwt_response.common_response',
    }

六 自定义签发和认证

6.1 drf-jwt自定义用户表签发

# 1 创建一个用户表
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)
    age = models.IntegerField()
    email = models.CharField(max_length=64)
    # 数据迁移
        

# 2 写登录接口,做token签发
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from .models import User

'''
源码中:
payload = jwt_payload_handler(user)
return {
    'token': jwt_encode_handler(payload),
    'user': user
}
'''
# 直接复制这3行代码
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserView(ViewSet):
    def login(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        # 查询用户
        user = User.objects.filter(username=username, password=password).first()
        if user:
            # 登录成功,签发token--->先背过,
            # 1 通过user,获取payload
            payload = jwt_payload_handler(user)
            print(payload)
            # 2 通过payload,得到token
            token = jwt_encode_handler(payload)
            return Response({'code': 100, 'msg': '登录成功', 'username': user.username, 'token': token})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})
        
# 3 登录接口签发token返回给前端  


# 4 定制过期时间
# djangorestframework-jwt的配置
import datetime

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), # 设置1天后过期, 默认5分钟过期

    'JWT_RESPONSE_PAYLOAD_HANDLER':'app01.jwt_response.common_response',
}

6.2 drf-jwt自定义认证类

# drf的认证类定义方式,之前学过
# 在认证类中,自己写逻辑
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .models import User

import jwt
from rest_framework_jwt.settings import api_settings
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER


class JWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        '''
        还可以加一个判断,如果没有携带token
        if not token:
        '''
        # token的位置在哪里,是你自己定的
        # 在请求头中携带token 
        token = request.META.get('HTTP_TOKEN')
        # 背过
        # 校验token是否过期,合法,如果都通过,查询出当前用户,返回
        # 如果不通过,抛异常
        try:
            payload = jwt_decode_handler(token)
            # 如果认证通过,payload就可以认为是安全的,我们就可以使用
            # payload是个字典形式
            user_id = payload.get('user_id')
            # 每个需要登录后,才能访问的接口,都会走这个认证类,一旦走到这个认证类,机会去数据库查询一次数据,会对数据造成压力?
            # user = User.objects.get(pk=user_id)
            # 优化后的
            # 方式一:
            # 优缺点:没有实际操作数据库,增加了速度。缺点:只能通过对象点属性查找出用户名和id,不能查找出其他数据。
            user = User(username=payload.get('username'), id=user_id)
            # 方式二:
            # 优缺点:直接保存一个字典,增加了代码效率。缺点:后续视图类中得到用户名时,就必须用request.user.get('username')了
            # user = {'username':payload.get('username'), 'id':user_id}

        except jwt.ExpiredSignature:
            # 有错,主动抛异常
            raise AuthenticationFailed('token过期')
        except jwt.DecodeError:
            raise AuthenticationFailed('解码失败')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('非法用户')
        # user_id取不到,就会直接抛异常
        except Exception:
            raise AuthenticationFailed('token认证异常')
        return user, token
	    # return (user, token)
    
    
# 局部使用
from .auth import JWTAuthentication

class BookView(APIView):
    authentication_classes = [JWTAuthentication]

    def get(self, request):
        print(request.user.age)
        return Response("你看到我了")
    
    
# 全局使用,
# setting.py
REST_FRAMEWORK = {
    # 认证模块
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'users.app_auth.JSONWebTokenAuthentication',
    ),
}


# 局部禁用
authentication_classes = []



# 如果没有drf认证类,可以把认证写在中间件中,或者写个装饰器,给每个函数都使用

七 源码分析

7.1 drf-jwt的签发源码分析

- 入口 from rest_framework_jwt.views import obtain_jwt_token

-2.
# obtain_jwt_token就是ObtainJSONWebToken.as_view()---> 视图类.as_view()
obtain_jwt_token = ObtainJSONWebToken.as_view()


-3 ObtainJSONWebToken是个视图类
# ObtainJSONWebToken ---> 继承了JSONWebTokenAPIView ---> 继承了APIView
# ObtainJSONWebToken在rest_framework_jwt的view.py文件中
class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer
   

-4 父类:JSONWebTokenAPIView
class JSONWebTokenAPIView(APIView):
    # 局部禁用掉权限和认证
    # 这是个登录接口
    permission_classes = ()
    authentication_classes = ()
    
    def get_serializer_class(self):
        return self.serializer_class
    
	# 继承了APIView,又写了GenericAPIView源码中拿到序列化类的两个方法,
    def get_serializer(self, *args, **kwargs):
        # 视图类中serializer_class
        serializer_class = self.get_serializer_class()
        return serializer_class(*args, **kwargs)
    
    # post请求,携带用户名和密码
    def post(self, request, *args, **kwargs):
        # serializer=JSONWebTokenSerializer(data=request.data)
        # 拿auth模块中自己写的序列化类,实例化对象
        serializer = self.get_serializer(data=request.data)
		# 调用序列化列的is_valid---> 字段自己的校验规则,局部钩子和全局钩子
        # serializer_class = JSONWebTokenSerializer
        # 读JSONWebTokenSerializer的局部或全局钩子 -------> 第5步
        if serializer.is_valid(): # 全局钩子在校验用户,生成token
            
            # 因为序列化类JSONWebTokenSerializer的全局钩子中返回了user和token,所以序列化对象就可以使用serializer对象.object.get('user') ----> 单表查询的第二种方式
            
            # 从序列化类中取出user
            user = serializer.object.get('user') or request.user
            # 从序列化类中取出token
            token = serializer.object.get('token')
            
            # 咱么定制返回格式的时候,写的就是这个函数,返回个字典
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
    
# 从这里进入:serializer_class = JSONWebTokenSerializer   
-5 JSONWebTokenSerializer的全局钩子
class JSONWebTokenSerializer(Serializer):
    # 全局钩子
    def validate(self, attrs):
        # attrs前端传入的,校验过后的数据 {username:lqz,password:lqz12345}
        
        # 把用户名和密码组成字典
        credentials = {
            # 写的比较高级,翻译出来就是:
            # 'username': attrs.get('usernme'),
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }
		
        # all()就是里面全部为真,才是True
        if all(credentials.values()): # 校验credentials中字典的value值是否都不为空
            # user=authenticate(username=前端传入的,password=前端传入的)
            # authenticate是,auth模块的用户名密码认证函数,可以传入用户名密码,去auth的user表中校验用户是否存在
            # 等同于:User.object.filter(username=username,password=加密后的密码).first()
            user = authenticate(**credentials)
            if user:
                if not user.is_active:  # 判断用户是否活跃,先不用管
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user)
                
			   # 通过全局钩子,返回user和token
                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)
            
            
# 总结:
1.传入的用户名和密码,去数据库校验(全局钩子中校验),校验成功签发token(全局钩子通过payload,得到token)
2.从序列化类中取出user、token
3.调用了你写的定制返回格式的函数,最终返回给前端

# 高级:调用了serializer.is_valid(),把校验和签发token的逻辑都写在了序列化类的全局钩子中了。

7.2 认证源码

# 入口:
from rest_framework_jwt.authentication import JSONWebTokenAuthentication


# 认证类,应该继承BaseAuthentication,重写authenticate方法。但是,这个JSONWebTokenAuthentication内部没有,那么父类中一定有authenticate方法
-1 父类中找:BaseJSONWebTokenAuthentication---> authenticate,找到了
class BaseJSONWebTokenAuthentication(BaseAuthentication):
    '''整个代码入口在这里'''
    def authenticate(self, request):
        # 拿到前端传入的token,前端传入的样子是  jwt token串  ----> 第2步
        jwt_value = self.get_jwt_value(request)
        # 如果前端没传,返回None,request.user中就没有当前登录用户
        # 如果前端没有携带token,也能进入到视图类的方法中执行,控制不住登录用户
        # 所以咱们才加了个权限类,来做控制
        if jwt_value is None:
            return None
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
        # 通过payload拿到当前登录用户
        # 只要payload可以解析出来,就说明荷载中的信息是正确的,是可以信任的
        user = self.authenticate_credentials(payload)
        return (user, jwt_value)
    
	def authenticate_credentials(self, payload):
        User = get_user_model()
        username = jwt_get_username_from_payload(payload)

        try:
            # 根据用户名查找到当前的用户对象
            # 等同于User.objects.filter(username)
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:
            msg = _('Invalid signature.')
            raise exceptions.AuthenticationFailed(msg)
        return user  # 返回当前登录用户
    
    
-2 JSONWebTokenAuthentication认证类
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    def get_jwt_value(self, request):
        # 切分后:auth是个列表 =['jwt','token串']  ---> 第3步
        auth = get_authorization_header(request).split()
        # jwt,前缀是可以定制的,在配置文件中
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
        
        # auth是空,返回None
        if not auth:
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None
        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None
        # 长度等于一,主动抛异常
        if len(auth) == 1:  
            msg = _('Invalid Authorization header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        # 长度大于2,也主动抛异常
        elif len(auth) > 2:
            msg = _('Invalid Authorization header. Credentials string '
                    'should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)
        # 长度必须等于2
        # 返回token串
        return auth[1] 
    
    
-3 get_authorization_header方法
	def get_authorization_header(request):
        # 去请求头中获取token串
        auth = request.META.get('HTTP_AUTHORIZATION', b'')
        # 但是请求头中的key,authonorization是不能更改的
        # 想要改,你就要重新写个类,继承JSONWebTokenAuthentication,内部自己写获取token的函数代码
        if isinstance(auth, str):
            auth = auth.encode(HTTP_HEADER_ENCODING)
        # auth=[jwt token串]
        return auth
    
    
# 如果用户不携带token,也能认证通过
# 所以我们必须加个权限类来限制

-4 权限类
class IsAuthenticated(BasePermission):
    def has_permission(self, request, view):
        # 如果用户是None就会返回False
        # is_authenticated表示认证通过,里面有值,就表示认证通过
        # 返回False,就是没有权限
        return bool(request.user and request.user.is_authenticated)
    
# 总结:
	1.如果前端没有传入token,返回None,request.user中就没有登录用户,但是也能够进入到视图类中,所以,必须加个权限类来限制

八 多方式登录

要求:

# 多方式登录接口
	-用户名+密码
    -邮箱+密码
    -手机号+密码
    
    
# 大坑:
    1 一开始没有使用auth的user表,做过迁移了
    2 后期再想扩展,其实就不行了
    3 如果真做了这个事,还要行
    	1 把所有的迁移文件删除(你的app,admin,auth)
        2 删库或者换库
        3 重新迁移

步骤

1.基于auth的user表扩写,继承AbstractUser,配置文件配置个参数AUTH_USER_MODEL
2.写登录接口,需要写个视图函数,自己写方法,映射路由,继承ViewSet
3.路由,映射
4.写序列化类,继承ModelSerializer,跟表模型有映射关系
    -1 重新自定义username字段,(源码中username字段是unique唯一的,如果不重写,字段自己的规则就过不去)
    -2 写全局钩子validate
    -3 取出前端传入的数据
    -4 查询用户是否存在,根据正则判断不同登录方式,再查询用户
    -5 判断用户存在和根据用户校验密码
    -6 签发token  ---> token的获取
    -7 把user和token两个数据保存再序列化对象中
    -8 返回校验后的数据attrs,字典形式
    -9 用户不存在,或者密码错误,主动抛异常
5.视图类中
    -1 先得到序列化对象
    -2 序列化校验
    -3 从序列化对象中取出user和token
    -4 返回Response

视图类

# 多方式登录接口,验证token写在序列化类中,使用auth的user表扩写
# 核心步骤:1.接收前端传来的参数,2.根据信息去用户表中查询,3.验证成功后签发token,4.返回user和token


from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from .serializer import UserSerializer


class UserView(ViewSet):
    def login(self, request):
        # 签发token的逻辑写在序列化类中
        # 此时的序列化类中只需要一个作用,反序列化的校验,并且是在全局钩子中校验用户是否存在

        # 1.得到序列化对象
        ser = UserSerializer(data=request.data)
        # 2.序列化校验,执行全局钩子
        if ser.is_valid():
            user = ser.user
            token = ser.token
            return Response({'code': 100, 'msg': '登录成功', 'username': user.username, 'token': token})
        else:  # 没有通过校验,返回明确的错误信息
            return Response({'code': 101, 'msg': ser.errors})

序列化文件(校验参数和签发token在全局钩子中)

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from .models import UserInfo
import re
# from rest_framework_jwt.views import obtain_jwt_token
# 找token:obtain_jwt_token--> ObtainJSONWebToken--> JSONWebTokenSerializer--> 全局钩子
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserSerializer(serializers.ModelSerializer):
    username = serializers.CharField()  # 这个优先用这个,就不是映射过来的,就没有unique的限制了

    class Meta:
        model = UserInfo
        # 继承Serializer,就没有这个错误了
        # 大坑:继承ModelSerializer后,fields的字段是映射过来的
        # username 是unique唯一的,反序列化校验的时候,字段自己的规则,会去数据库查询有没有这个用户,如果有,直接报错了
        fields = ['username', 'password']  # 只有前端两个框中的数据,做反序列化的校验

    # 全局钩子
    # 全局钩子的返回值必须是个字典,是校验过后的数据
    def validate(self, attrs):
        # attrs是前端传过来的数据,经过字段自己规则、局部钩子的校验的数据。  是什么类型?
        # 无论以什么方式登录,前端的页面中只有两个输入框,一个用username接收,一个用password接收
        # 第一个参数使用正则来匹配,看是哪种方式
        # 1.取出前端传入的数据
        username = attrs.get('username')  # 可能是用户名,手机号,邮箱
        password = attrs.get('password')

        # 2.查询用户是否存在(先查出用户再校验密码)
        # 不同的登陆方式,直接使用正则匹配,来匹配第一个参数到底是哪种方式(手机号,邮箱,用户名)
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = UserInfo.objects.filter(phone=username).first()
        elif re.match(r'^.+@.+$', username):
            user = UserInfo.objects.filter(email=username).first()
        else:
            user = UserInfo.objects.filter(username=username).first()
        # 3.用户存在,并且根据用户校验密码
        if user and user.check_password(password):
            # 5.签发token
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)

            # 6.把两个参数保存在序列化对象中
            self.user = user
            self.token = token

            # 返回校验后的数据,字典形式
            return attrs

        # 4.用户不存在,或者密码错误,主动抛异常
        else:
            raise ValidationError('用户名或密码错误')

表模型

from django.db import models
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):
    phone = models.CharField(max_length=32)

路由

from app01 import views

urlpatterns = [
    path('user/login/', views.UserView.as_view({'post': 'login'})),
]
posted @ 2023-05-30 20:21  星空看海  阅读(246)  评论(0编辑  收藏  举报