在Django+DRF中实现token验证

1,451 阅读7分钟

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.更加简洁的写法(封装)

目录树:

image.png

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 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。