1.使用pyjwt
在使用之前看JWT(JSON Web Token) 原理简析深入理解
看关于token的保存JWT(JSON Web Token) 原理简析深入理解
在实现token功能验证时,需要先进行pyjwt的下载
命令:pip install pyjwt
其他的包,例如使用djangorestframework-jwt其实是封装了pyjwt的功能,本质上还是使用了pyjwt,而且pyjwt不仅能用在Django项目中,还能使用在flask等项目中,相比较djangorestframework-jwt更加灵活。
2.创建用户数据库以及序列化器
# models.py
from django.db import models
class UserInfo(models.Model):
""" 用户名字段 """
username = models.CharField(max_length=32)
""" 密码字段 """
password = models.CharField(max_length=20)
class Meta:
db_table = 'UserInfo' # 指明数据库表名
# serializers.py
from rest_framework.serializers import ModelSerializer
from .models import UserInfo
class UserInfoSerializer(ModelSerializer):
class Meta():
model = UserInfo
fields = "__all__"
3.创建两个视图
首先创建子应用account
子应用下创建视图JwtLogin用于验证用户并且创造token
子应用下创建视图JwtOrder用于验证token,验证成功则就能成功Response
3.1路径配置
# 主 urls.py
from django.urls import path,re_path,include
urlpatterns = [
# path('admin/', admin.site.urls),
re_path('',include('accounts.urls'))
]
# accounts/urls.py
from django.urls import re_path
from . import views
urlpatterns = [
re_path('^user/$',views.JwtLogin.as_view()),
re_path('^order/$',views.JwtOrder.as_view()),
]
3.2视图实现
首先是登陆视图
# account/views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from django.conf import settings
from .models import UserInfo
from .serializers import UserInfoSerializer
import jwt
from jwt import exceptions
import datetime
class JwtLogin(APIView):
def post(self,request):
""" 首先是接收前端传入的用户名以及密码 """
username = request.data.get('username')
password = request.data.get('password')
""" 对传入的用户名以及密码与数据库进行比对 """
User = UserInfo.objects.filter(username = username,password = password)
if not User:
""" 比对失败,返回错误信息【用户不存在】 """
return Response({"msg":"查询的用户不存在"})
else:
""" 比对成功,进行序列化 """
i = UserInfoSerializer(instance=User,many=True)
# 下面开始创建token
""" 第一部分是 headers 为固定参数 """
headers = {
'typ':'jwt', # token类型
'alg':'HS256' #指的是加密算法为HS256算法
}
""" 第二部分是 载荷 """
payload = {
'id':dict(i.data[0]).get('id'),
'username': dict(i.data[0]).get('username'),
# exp 为有效时间,表示token的有效时间
'exp' : datetime.datetime.utcnow() + datetime.timedelta(minutes=1)
}
""" 最后形成token并赋予给result:
传入headers以及payloads进行加密
salt为加密,他将会与第一部分+第二部分一起进行加密,这里salt的值为settings文件里的SECRET_KEY
algorithm将传入加密方式,这里是HS256
"""
result = jwt.encode(headers=headers,payload=payload,key=settings.SECRET_KEY,algorithm="HS256").decode('utf-8')
"""最终返回登录成功的信息以及token """
return Response({
"msg":"您已登录成功,token已更新",
"token":result
})
然后是访问视图
# account/views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from django.conf import settings
from .models import UserInfo
from .serializers import UserInfoSerializer
import jwt
from jwt import exceptions
import datetime
class JwtOrder(APIView):
def get(self,request):
""" 访问本视图的时候前端使用get方法并传入参数token"""
token = request.query_params.get('token')
verified_payload = None
msg = None
try:
""" 对token进行验证,True参数代表jwt内部进行验证,包括对token过期时间的验证 """
verified_payload = jwt.decode(token,salt,True)
""" 假如对token的验证有异常,则可以使用下面的异常类 """
except exceptions.ExpiredSignatureError:
msg = 'token已经失效'
except jwt.DecodeError:
msg = 'token认证失败'
except jwt.InvalidIssuer:
msg = 'token已经失效'
if not verified_payload:
return Response({"msg":msg})
""" 解密后的token,暴露出了原来的载荷 payload
输出结果为字典:
{'id': 1, 'username': 'lihua', 'exp': 1647163201}
"""
print(verified_payload)
return Response("成功访问")
4.更加简洁的写法(封装)
目录树:
4.1登录接口的封装
在子应用accounts下创建python包utils,包内创建jwt_auth.py
# jwt_auth.py
import datetime
from django.conf import settings
import jwt
# 参数1需要传入payload参数作为加密的第二部分,参数2为默认超时时间,为1分钟
def create_token(payload,timeout = 1):
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(timeout)
# 形成token
result = jwt.encode(headers=headers, payload=payload, key=settings.SECRET_KEY, algorithm="HS256").decode('utf-8')
return result
简化后的登录视图:
# accounts/views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import AuthenticationFailed
from django.conf import settings
from .models import UserInfo
from .serializers import UserInfoSerializer
import jwt
from jwt import exceptions
import datetime
#这里使用工具包里面的create_token函数
from .utils.jwt_auth import create_token
class ProLogin(APIView):
def post(self,request):
username = request.data.get('username')
password = request.data.get('password')
user = UserInfo.objects.filter(username = username,password = password)
serializer = UserInfoSerializer(instance=user,many=True)
payload = {
'username':dict(serializer.data[0]).get('username'),
'id':dict(serializer.data[0]).get('id'),
}
result = create_token(payload,2)
if result:
return Response({
'msg':'登录成功',
'token':result,
})
else:
return Response({
'msg': '登录失败',
})
4.2访问视图的封装
在auth.py内创建一个验证类
关于认证组件可以学习BaseAuthentication使用
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from django.conf import settings
import jwt
from jwt import exceptions
""" 创建一个类继承BaseAuthentication """
class JwtQueryparamsAuthentication(BaseAuthentication):
""" 继承了BaseAuthentication的类会自动调用内部的authenticate函数,
因此要自己在里面写这个函数
"""
def authenticate(self, request):
token = request.query_params.get('token')
verified_payload = None
msg = None
try:
# 对token进行验证,True参数代表jwt内部进行验证,包括对token过期时间的验证
verified_payload = jwt.decode(token, settings.SECRET_KEY, True)
except exceptions.ExpiredSignatureError:
msg = 'token已经失效'
raise AuthenticationFailed({'msg':msg}) #抛出异常
except jwt.DecodeError:
msg = 'token认证无语'
raise AuthenticationFailed({'msg': msg}) # 抛出异常
except jwt.InvalidIssuer:
msg = 'token已经失效'
raise AuthenticationFailed({'msg': msg}) # 抛出异常
""" 认证完毕后有三种操作 """
# 1.抛出异常,后续不再执行
""" 上面已经抛出异常 """
""" 2.return 一个元组 (1,2)
认证通过 在视图中如果调用request.user就是元组的第一个值,
request.auth就是元组的第二个值
"""
return (verified_payload,token)
# 3.None
""" 暂时不用管 """
实现访问视图:
# accounts/views.py
# 此处使用上面自定义的验证类
from .extensions.auth import JwtQueryparamsAuthentication
# 增强型访问接口
class ProOrder(APIView):
""" 这条语句表示在执行 get...等行为时要先执行这个类里的authenticate函数 """
authentication_classes = [JwtQueryparamsAuthentication]
def get(self,request):
try:
""" 这里的request.user是验证类中自动执行函数中传来的元组的第一个值 """
user = request.user
return Response(user)
except AuthenticationFailed as e:
return Response({
e.value
})
5.pyjwt总结
关于pyjwt,无非就是两个方法
jwt.encode进行编码
jwt.docode进行解码
6.注意点
参考文章:深入了解jwt方案的优缺点
注意:使用jwt时,token保存在客户端,而服务器不保存token,因此不需要将token储存在数据库或者redis内,而token 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。那么,我们如何解决这个问题呢?
- 将 token 存入内存数据库:将 token 存入 DB 中,redis 内存数据库在这里是是不错的选择。如果需要让某个 token 失效就直接从 redis 中删除这个 token 即可。但是,这样会导致每次使用 token 发送请求都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则。
- 黑名单机制:和上面的方式类似,使用内存数据库比如 redis 维护一个黑名单,如果想让某个 token 失效的话就直接将这个 token 加入到 黑名单 即可。然后,每次使用 token 进行请求的话都会先判断这个 token 是否存在于黑名单中。
- 修改密钥 (Secret) : 我们为每个用户都创建一个专属密钥,如果我们想让某个 token 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大,比如:1⃣️如果服务是分布式的,则每次发出新的 token 时都必须在多台机器同步密钥。为此,你需要将必须将机密存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。2⃣️如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
- 保持令牌的有效期限短并经常轮换:很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。 对于修改密码后 token 还有效问题的解决还是比较容易的,说一种我觉得比较好的方式:使用用户的密码的哈希值对 token 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。