DRF从入门到精通八(Simplejwt快速使用、定制返回格式、Simplejwt默认配置、多方式登录、自定义表签发token、编写认证类)

通过上一篇博客对jwt的介绍以及开发重点的了解,接下来我们就使用第三方模块djangorestframework-simplejwt来实现。

通过对jwt的开发重点我们知道主要分为两个部分,签发和认证

	-签发----》登录成功后进行签发
		第三方模块djangorestframework-simplejwt帮我们做好了,只需要传入注册的user
		'djangorestframework-simplejwt是使用auth_user表来进行用户数据编写的'
			'自定义接口使用,签发token'
				refresh = TokenObtainPairSerializer.get_token(user)
				-refresh是一个对象----refresh:str(refresh)/access:str(refresh.access_token)
				
	-认证---》认证类,它重写了authenticate,在内部完成了认证,如果认证通过,返回两个值
		-simple-jwt提供了:JWTAuthentication,必须配合权限类一起使用
        -只要用户带了token,才验证,不带就不验证

一、djangorestframework-simplejwt快速使用

JWT主要用于签发登录接口需要配合认证类 JWT目前有两种 JtwSimplejwt(jwt比较老了 simple现在比较流行,所以这里我就使用simplejwt了。)

1.基础使用步骤

  1. 安装:建议使用pycharm可以安装到指定解释器
	pip install djangorestframework-simplejwt
  1. simplejwt默认使用auth_user表签发token,所以我们直接新建项目后执行迁移命令
	-makemigrations
	-migrate
  1. 创建一个超级用户:createsuperuser
  2. 签发登录:只需要在路由中配置(simplejwt帮我们写好了登录接口以及权限类)

路由配置

	'导入模块'
	from rest_framework_simplejwt.views import token_obtain_pair, token_verify, token_refresh
	urlpatterns = [
	    path('login/', token_obtain_pair),  # 登录  签发token
	    path('verify/', token_verify),  # 验证token 是否有效
    	path('refresh/', token_refresh),  # 刷新token
	]

settings配置

	'注册app'
	INSTALLED_APPS = [
	    ...
	    'rest_framework_simplejwt',
	    ...
	]

	
	import datetime
	SIMPLE_JWT = {
	    # token有效时长
	    'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=30),
	    # token刷新后的有效时间
	    'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
	}

此时直接访问即可,它都帮我们写好了,在请求体中携带刚刚创建的超级用户的账号密码就会返回token(因为是simplejwt它是双Token认证)

在这里插入图片描述

因为是双token认证,获取到的access才是真正使用的token,而refresh则是用于更新access。因为access过期时间很短,过期后就需要重新生成access的token保证token的安全,所以就需要使用refresh用来变更新的有效token


验证有效期token

进入 http://127.0.0.1:8000/api/v1/verify/,下面提示输入Token, 输入刚刚登录认证后获取到的access的token值,验证成功。注意,验证成功没有提示信息返回(空字典),只有一个200的响应码

在这里插入图片描述


变更新的有效token

进入 http://127.0.0.1:8000/api/v1/refresh/,下面提示填写refresh,在里面填写登录认证后获取到的refresh的token值,如果填写的正确,则会获取到新的Token,否则会提示验证失败

在这里插入图片描述


2.自定义视图类校验访问

我们定义了一个book视图类,它只允许访问时在请求头里面携带了合法的token值才能通过认证。

路由配置

	from rest_framework_simplejwt.views import token_obtain_pair, token_verify
	urlpatterns = [
	    path('login/', token_obtain_pair),  # 登录  签发token
	    path('verify/', token_verify),  # 验证token 是否有效
	    path('refresh/', token_refresh),  # 刷新token
	]

局部配置认证及权限类

视图配置

	from rest_framework.views import APIView
	from rest_framework_simplejwt.authentication import JWTAuthentication
	from rest_framework.permissions import IsAuthenticated
	from rest_framework.response import Response
	
	'局部配置,必须配合权限类'
	class BookView(APIView):
	    '''配置的Jwt认证,但是得在headers添加,如果不添加的话,就不会生效,添加才可生效'''
	    authentication_classes = [JWTAuthentication]  # 登录认证
	    permission_classes = [IsAuthenticated]  # 配置了权限类,没登录的就没有权限访问了
	    '一旦配置了去认证类和权限类后,refresh的token就无法使用,会显示令牌类型错误,只能使用access的token'
	
	    def get(self,reqeust):
	        return Response({'测试测试'})
	
	'''
	这个时候直接访问我们的接口,就会发生错误,"detail":"身份认证信息未提供"
	因为我们访问的时候需要带上simplejwt的token
	固定格式为:Authorization:Bearer 注意这里哟一个空格,在空格后面填写签发过的token
	'''

全局配置认证及权限类

settings中配置

	# 全局配置
	REST_FRAMEWORK = {  '它自己内置的登录是哪怕配置了全局也不会进行认证,源码中进行了禁用'
	    'DEFAULT_AUTHENTICATION_CLASSES': [
	        'rest_framework_simplejwt.authentication.JWTAuthentication'
	    ],
	    'DEFAULT_PERMISSION_CLASSES': [
	        'rest_framework.permissions.IsAuthenticated',
	    ],
	}

携带登录后,服务端响应给我们的token值来访问,token值的开头必须是Bearer+空格,因为在源码内部获取校验token值前,会先通过空格进行分隔一下,第一个值是否为Bearer,如果是的话才会获取空格后面的token值来进行校验,所以我们后续会重写一些方法,不需要遵守一些不必要的规则

在这里插入图片描述

注意:只要当前的access的token没过期,而之前签发的access的token和后来刷新签发的access的token都可以使用。前提是没有配置权限类,如果配置了使用refresh的token,则不会有用,会显示类型错误。

3.关于双token认证问题

	1)单token
		-用户登录后----->签发token---->但是有过期时间
			1.设置太短的token过期时间,如:3 minute后就需要重新登录,体验太差,一天啥事不干就重新登录
			2.设置太长的token过期时间,如:7 day,7天都不需要登录----->容易被人截获到长时间使用--->不安全
	2)双token
		-用户登录后---->签发两个token----->目前的verify检验接口,只要是它签发的token,都会认证通过
			例如:access:过期时间短 3分钟
			例如:refresh:过期时间长 7-用户正常用,都会用access,不会用refresh
		-access过会有过期了,一旦过期就用不了了---->然后可以通过refresh这个token调用刷新接口,在签发一个access的token
		-通过refresh再次签发的token这个过程,是不需要登录的,这对用户是无感知的
		-后续再使用access这个token发请求

	'配置认证类,就不能使用refresh的token进行校验登录了' 

	'''
	双认证的好处就是,一旦access的token被别人截取到了,拿着模拟发请求,只能在有效时间内使用,
	因为access的token很快会过期,这样就保障安全
	'''

二、定制返回格式

继承auth_user表完成签发登录,但是它的返回格式太固定了只有Token,但是我们想自定义格式呢?

	如:
	{
		'code': 100,
	    'msg': '登录成功',
	    'username': self.user.username,
	    'token':'fdsafsfsafsadf'
    }

1.写个序列化类,重写validate ,返回什么,前端看到什么

	from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
	
	class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
		'在荷载中添加自定义内容'
	    @classmethod
	    def get_token(cls, user):
	        token = super().get_token(user)  # 签发用户
	        token['name'] = user.username  # 往荷载里面添加用户名称
	        return token
		'''重写get_token方法,它返回的token中就是荷载的内容'''
		
		'自定义返回格式'
	    def validate(self, attrs):
	        old_data = super().validate(attrs)
	        data = {'code': 100,
	                'msg': '登录成功',
	                'username': self.user.username,
	                'refresh': old_data['refresh'],
	                'access': old_data['access']
	                }
	        return data

2.在settings配置文件中配置

	SIMPLE_JWT = {
	    "TOKEN_OBTAIN_SERIALIZER": "app01.serializer.MyTokenObtainPairSerializer",
	}

效果如下
在这里插入图片描述


三、Simple JWT的默认设置

	# JWT配置
	SIMPLE_JWT = {
	    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),  # Access Token的有效期
	    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),  # Refresh Token的有效期
	    
	    # 对于大部分情况,设置以上两项就可以了,以下为默认配置项目,可根据需要进行调整
	    
	    # 是否自动刷新Refresh Token
	    'ROTATE_REFRESH_TOKENS': False,  
	    # 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
	    'BLACKLIST_AFTER_ROTATION': False,  
	    'ALGORITHM': 'HS256',  # 加密算法
	    'SIGNING_KEY': settings.SECRET_KEY,  # 签名密匙,这里使用Django的SECRET_KEY
	    # 如为True,则在每次使用访问令牌进行身份验证时,更新用户最后登录时间
	    "UPDATE_LAST_LOGIN": False, 
	    # 用于验证JWT签名的密钥返回的内容。可以是字符串形式的密钥,也可以是一个字典。
	    "VERIFYING_KEY": "",
	    "AUDIENCE": None,# JWT中的"Audience"声明,用于指定该JWT的预期接收者。
	    "ISSUER": None, # JWT中的"Issuer"声明,用于指定该JWT的发行者。
	    "JSON_ENCODER": None, # 用于序列化JWT负载的JSON编码器。默认为Django的JSON编码器。
	    "JWK_URL": None, # 包含公钥的URL,用于验证JWT签名。
	    "LEEWAY": 0, # 允许的时钟偏差量,以秒为单位。用于在验证JWT的过期时间和生效时间时考虑时钟偏差。
	    # 用于指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer"
	    "AUTH_HEADER_TYPES": ("Bearer",), 
	    # 包含JWT的HTTP请求头的名称。默认为"HTTP_AUTHORIZATION"
	    "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 
	     # 用户模型中用作用户ID的字段。默认为"id"。
	    "USER_ID_FIELD": "id",
	     # JWT负载中包含用户ID的声明。默认为"user_id"。
	    "USER_ID_CLAIM": "user_id",
	    
	    # 用于指定用户身份验证规则的函数或方法。默认使用Django的默认身份验证方法进行身份验证。
	    "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
	    #  用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"。
	    "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
	    # JWT负载中包含令牌类型的声明。默认为"token_type"。
	    "TOKEN_TYPE_CLAIM": "token_type",
	    # 用于指定可以使用的用户模型类。默认为"rest_framework_simplejwt.models.TokenUser"。
	    "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
	    # JWT负载中包含JWT ID的声明。默认为"jti"。
	    "JTI_CLAIM": "jti",
	    # 在使用滑动令牌时,JWT负载中包含刷新令牌过期时间的声明。默认为"refresh_exp"。
	    "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
	    # 滑动令牌的生命周期。默认为5分钟。
	    "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
	    # 滑动令牌可以用于刷新的时间段。默认为1天。
	    "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
	    # 用于生成访问令牌和刷新令牌的序列化器。
	    "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
	    # 用于刷新访问令牌的序列化器。默认
	    "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
	    # 用于验证令牌的序列化器。
	    "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
	    # 用于列出或撤销已失效JWT的序列化器。
	    "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
	    # 用于生成滑动令牌的序列化器。
	    "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
	    # 用于刷新滑动令牌的序列化器。
	    "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
	}

四、自定义登录和认证(多方式登录auth_user表)

1) 面条版(使用APIView编写)

路由urls.py

	from django.urls import path
	from . import views
	
	urlpatterns = [
	    path('users/', views.UserView.as_view()),
	]

视图层views.py

	from rest_framework.views import APIView
	import re
	from . import models
	from rest_framework.response import Response
	from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
	
	class UserView(APIView):
	    '''全局配置了,所以局部禁用'''
	    authentication_classes = ()
	    permission_classes = ()
	    def post(self, request):
	        # 1.取出用户数据
	        username = request.data.get('username')
	        password = request.data.get('password')
	
	        # 判断是否是邮箱、手机号还是用户名
	        if re.match(r'^1[3-9][0-9]{9}$]',username):
	            user = models.User.objects.filter(moblie=username).first()
	        elif re.match(r'^.+@.+$',username):
	            user = models.User.objects.filter(email=username).first()
	        else:
	            user = models.User.objects.filter(username=username).first()
			
			# 校验密码通过后执行
	        if user and user.check_password(password):
	            # 签发token
	            refresh = TokenObtainPairSerializer.get_token(user)
	            # 返回给前端
	            return Response({'code':100,'message':'登录成功','access':str(refresh.access_token),'refresh':str(refresh)})
	        else:
	            return Response({'code': 101, 'message': '用户名或密码错误'})

2) 升级版(使用GenericAPIView编写)

序列化类(主要用于校验)

	from rest_framework import serializers
	import re
	from . import models
	from rest_framework.exceptions import ValidationError
	class UserSerializer(serializers.Serializer):
		'主要为校验登录'
	    username = serializers.CharField()
	    password = serializers.CharField()
	
	    def validate(self, attrs):
	        username = attrs.get('username')
	        password = attrs.get('password')
	
	        if re.match(r'^1[3-9][0-9]{9}$', username):
	            user = models.User.objects.filter(moblie=username).first()
	        elif re.match(r'^.+@.+$',username):
	            user = models.User.objects.filter(email=username).first()
	        else:
	            user = models.User.objects.filter(username=username).first()
			
	        if user and user.check_password(password):
	        	'签发token'
	            refresh = TokenObtainPairSerializer.get_token(user)
	            
	            # 有漏洞方法一:self中如果有重名的属性,则会替换掉,会污染掉这个serializer类的对象
	            # self.refresh = refresh
	            # self.username = user.username
	
	            # 无漏洞复杂方法二
	            self.context['refresh'] = str(refresh)
	            self.context['access'] = str(refresh.access_token)
	            self.context['username'] = user.username
	            return attrs
	        else:
	            raise ValidationError('用户名或密码错误')

视图层views.py

	from rest_framework.generics import GenericAPIView
	from .serializer import UserSerializer
	class UserView(GenericAPIView):
	    authentication_classes = ()
	    permission_classes = ()
	    serializer_class = UserSerializer
	    def post(self,request):
	    	# ser = UserSerializer(data=request.data)
	        ser = self.get_serializer(data=request.data)
	        
	        if ser.is_valid():  # 当校验通过执行,自己的验证规则,局部钩子,全局钩子
	            '对应serializer类中的漏洞方法一'
	            # access = str(ser.refresh.access_token)
	            # refresh = str(ser.refresh)
	            # username = ser.username
	
	            '无漏洞复杂方法二,context是视图类与序列化类之间沟通的桥梁'
	            access = ser.context.get('access')
	            refresh = ser.context.get('refresh')
	            username = ser.context.get('username')
	            return Response({
	                'code': '100',
	                'message': '登录成功',
	                'username': username,
	                'refresh': refresh,
	                'access': access,
	            })
	        else:  # 校验不通过执行
	            return Response({'code':101,'message':'用户名或密码错误'})

3) 优化版(使用GenericAPIView编写)

序列化类(主要用于校验)

	from rest_framework import serializers
	import re
	from . import models
	from rest_framework.exceptions import ValidationError
	
	class UserSerializer(serializers.Serializer):
	    username = serializers.CharField()
	    password = serializers.CharField()

	    def validate(self, attrs):
	        username = attrs.get('username')
	        password = attrs.get('password')
	
	        if re.match(r'^1[3-9][0-9]{9}$', username):
	            user = models.User.objects.filter(moblie=username).first()
	        elif re.match(r'^.+@.+$',username):
	            user = models.User.objects.filter(email=username).first()
	        else:
	            user = models.User.objects.filter(username=username).first()
	
	        if user and user.check_password(password):
	        	'签发token'
	            refresh = TokenObtainPairSerializer.get_token(user)
	            
	            data = {
	                'code':100,
	                'message':'登录成功',
	                'username': user.username,
	                'refresh':str(refresh),
	                'access':str(refresh.access_token),
	            }
	            return data
	        else:
	            raise ValidationError('用户名或密码错误')

视图层views.py

	from rest_framework.generics import GenericAPIView
	from .serializer import UserSerializer
	
	class UserView(GenericAPIView):
	    authentication_classes = ()
	    permission_classes = ()
	    serializer_class = UserSerializer
	    def post(self,request):
	        ser = self.get_serializer(data=request.data)
	        if ser.is_valid():  # 当校验通过执行,自己的验证规则,局部钩子,全局钩子
	            data = ser.validated_data
	            return Response(data)
	        else:  # 校验不通过执行
	            return Response({'code':101,'message':'用户名或密码错误'})

效果展示
在这里插入图片描述


4) 补充 研究simple-jwt提供的Token类

	从上面自定义登录中可以发现一直在用refresh = TokenObtainPairSerializer.get_token(user)签发token
	'这些都是通过查看源码发现的'
	-1 RefreshToken:生成refresh token的类
	-2 AccessToken:生成refresh token的类
	-3 Token:它们俩的父类
	-4 str(RefreshToken的对象)---》得到字符串 refresh token,为什么可以直接得到因为Token类中写了__str__方法
	-5 str(RefreshToken的对象.access_token)---》得到字符串 access token,是因为Token类中写了__str__方法
		RefreshToken类中的access_token世方法被包装成了数据属性,它的返回值是AccessToken的对象
	-6 RefreshToken,AccessToken的对象,都能点出payload荷载
	-7 RefreshToken,AccessToken的对象,都能通过中括号取出荷载中的值

	'魔法方法之 . 操作:'
		-__getattr__
	    -__setattr__
	    --------点 拦截 -----
	    对象.name  当属性不存在时,触发__getattr__执行
	    对象.name='xxx' 当属性不存在时,触发__setattr__的执行

	'魔法方法之 [] 操作:'
		-__getitem__
	    -__setitem__
	    --------[] 拦截 -----
	    对象['name']  当属性不存在时,触发__getitem__执行
	    对象['name']='xxx' 当属性不存在时,触发__setitem__执行

五、基于自定义表签发token

序列化类(主要用于校验)

	'''使用自定义用户表校验登录签发token'''
	from rest_framework import serializers
	from rest_framework.exceptions import APIException
	from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
	from . import models
	from rest_framework_simplejwt.tokens import RefreshToken
	from django.db.models import Q
	
	class UserInfoSerializer(serializers.Serializer):
	    username = serializers.CharField()
	    password = serializers.CharField()
	
	
	    def validate(self, attrs):
	        username = attrs.get('username')
	        password = attrs.get('password')
	        try:
	            phone = int(username)
	        except:
	            phone = 0
	
	        condition = Q(username=username) | Q(email=username) | Q(phone=phone)
	        user = models.UserInfo.objects.filter(condition, password=password).first()
	        if user:
	            # refresh = TokenObtainPairSerializer.get_token(user)
	            refresh = RefreshToken.for_user(user)
	            return {
	                'code':100,
	                'message':'登录成功',
	                'username':username,
	                'refresh':str(refresh),
	                'access':str(refresh.access_token),
	            }
	        else:
	            raise APIException({'code': 101, 'message': '用户名或密码错误'})

路由urls.py

	from rest_framework.routers import SimpleRouter
	router = SimpleRouter()
	router.register('user',views.UserModelView,'user')
	urlpatterns = []
	urlpatterns += router.urls

视图类views.py

	from rest_framework.viewsets import GenericViewSet
	from .serializer import UserInfoSerializer
	from rest_framework.decorators import action
	from rest_framework.response import Response
	
	class UserModelView(GenericViewSet):
	    authentication_classes = ()
	    permission_classes = ()
	    serializer_class = UserInfoSerializer
	
	    @action(methods=['POST'], detail=False)
	    def login(self, request):
	        ser = self.get_serializer(data=request.data)
	        ser.is_valid(raise_exception=True)
	        return Response(ser.validated_data)

效果展示

在这里插入图片描述


六、基于自定义表编写认证类

自定义认证类

	from rest_framework.authentication import BaseAuthentication
	from rest_framework.exceptions import APIException
	from . import models
	from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
	from rest_framework_simplejwt.authentication import JWTAuthentication
	from rest_framework_simplejwt.tokens import AccessToken
	
	class LoginAuthenticate(BaseAuthentication):
	    def authenticate(self, request):
	        # 取出前端的token,它放在哪里了?这个是后端规定的,我们定死在请求头中,以token作为key,value值为登录获取的token
	        token = request.META.get('HTTP_TOKEN')
	        if token:
	            try:
	                # 拿到了token,需要检验token是否合法,是否被篡改/伪造,是否过期,如果都通过,根据payload中的user_id取出当前用户。
	                '''我们可以去simplejwt的认证类中看它是怎么写的'''
	                '''validated_token =get_validated_token(token)--->返回了AccessToken(token)传入的对象'''
	                validated_token = AccessToken(token)  # 从源码中读出来的
	            except Exception as e:
	                raise APIException({'code': 999, 'message': str(e)})
	
	            '''拿到token串对应的用户id'''
	            # user = validated_token.payload['user_id']
	            # user_id = validated_token['user_id']
	            user = models.UserInfo.objects.filter(pk=validated_token['user_id']).first()
	
	            return user, token
	        else:
	        	 # drf的异常会被全局异常捕获,当然也可以自己自定义全局异常
	            raise APIException({'code': 101, 'message': 'token必须携带'}) 

urls.py

	from rest_framework.routers import SimpleRouter
	router = SimpleRouter()
	router.register('books',views.BookView,'books')
	urlpatterns = []
	urlpatterns += router.urls

视图层views.py

	from .auth import LoginAuthenticate
	class BookView(GenericViewSet):
	    authentication_classes = [LoginAuthenticate]
	    '''之前配置了全局权限类,所以这里得局部禁用一下'''
	    permission_classes = []
	    def list(self, request):
	        return Response('好好多多书啊')

效果展示

在这里插入图片描述

  • 14
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值