DRF从入门到精通八(Simplejwt快速使用、定制返回格式、Simplejwt默认配置、多方式登录、自定义表签发token、编写认证类)
此时直接访问即可,它都帮我们写好了,在请求体中携带刚刚创建的超级用户的账号密码就会返回token(因为是simplejwt它是双Token认证),如果是的话才会获取空格后面的token值来进行校验,所以我们后续会重写一些方法,不需要遵守一些不必要的规则。我们定义了一个book视图类,它只允许访问时在请求头里面携带了合法的token值才能通过认证。,因为在源码内部获取校验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目前有两种
Jtw
和Simplejwt
(jwt比较老了 simple现在比较流行,所以这里我就使用simplejwt了。)
1.基础使用步骤
- 安装:建议使用pycharm可以安装到指定解释器
pip install djangorestframework-simplejwt
- simplejwt默认使用
auth_user表签发token
,所以我们直接新建项目后执行迁移命令
-makemigrations
-migrate
- 创建一个超级用户:
createsuperuser
- 签发登录:只需要在路由中配置
(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('好好多多书啊')
效果展示
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)