DRF中使用jwt 认证和token认证的方法及区别

1,911 阅读5分钟

token

token介绍

token 直译为令牌, 一般是用户登录成功之后, 服务端生成的唯一字符串(这个字符串不包含任何的用户信息), 并返回给客户端, 客户端需要访问服务器数据的时候只需要携带着这个字符串就可以访问了, 不再需要输入账户密码。 一般我们都会给token设置有效时间, 过了有效期则需要重新登录获取新的token

token 验证流程
  1. 从用户的请求头或cookie中获取token
  2. 将获取到的token到数据库查询
  3. 查询成功认证成功,查询失败或超时则认证失败

一个简单的token使用例子

生成token的函数
def create_token(mobile):
    """ 根据用户手机号生成用户token """
    import hashlib
    import time
    ctime = str(time.time())
    token = hashlib.md5(bytes(mobile, encoding='utf-8'))
    token.update(bytes(ctime, encoding='utf-8'))
    return token.hexdigest()
用户登录视图
class UserLoginView(APIView):
    """
        User 登录接口, 登录成功之后返回token
    """
    authentication_classes = []
    permission_classes = []
    def post(self, request, *args, **kwargs):
        rsp = {"code": 10000, "msg": "success"}
        # 请求数据校验
        ser = UserLoginSerializer(data=request.data)
        if ser.is_valid(raise_exception=True):
            rsp["token"] = ser.token
        else:
            rsp["code"] = 10001
            rsp["msg"] = ser.errors
        return Response(rsp)
登录数据校验的serializer
class UserLoginSerializer(serializers.Serializer):
    """
        登录接口请求数据验证
    """
    mobile = serializers.CharField(write_only=True)
    password = serializers.CharField(write_only=True)
    
    class Meta:
        model = Users
        fields = ['mobile', 'password']
    
    def validate(self, attrs):
        # 设置校验的数据
        mobile = attrs.get("mobile")
        password = attrs.get("password")

        if re.match(REGEX_MOBILE, mobile):  # 正则匹配手机号  进行手机登录
            user_obj = Users.objects.filter(mobile=mobile).first() 
        else:
            raise serializers.ValidationError({"user":"用户手机号格式错误"})
            
        if not user_obj:
            raise serializers.ValidationError({"message":"%s 未注册"%mobile})
        
        # 登录验证
        if user_obj and check_password(password, user_obj.password):
            token = create_token(mobile)
            Tokens.objects.create(user=user_obj, token=token, created=datetime.now())
            user_obj.last_login_time = datetime.now()
            user_obj.save()
            self.user = user_obj
            self.token = token
            return attrs
        else:
            raise serializers.ValidationError({"message":"手机号或密码错误"})
用户登录成功之后的每次请求,都需要判断用户是否携带token来访问服务器, 即请求认证模块
class UserAuthenticate(BaseAuthentication):
    """
        用户认证类,token有效期 48 小时
    """
    def authenticate(self, request):
        ret = {"code":200,"msg":None}
        # 从请求头中拿 AUTHENTICATION
        token = request._request.META.get("HTTP_AUTHORIZATION")    
        if token:
            user_info = Tokens.objects.filter(token=token).first()
            if user_info:
                # 我这里是直接计算时间差判断是否过期, 也可以将token存到redis设置过期时间
                two_day_ago = datetime.now() - timedelta(hours=48, minutes=0, seconds=0)  # 获取两天之前的时间
                if user_info.created > two_day_ago:
                    return (user_info.user, user_info)    # token 认证成功
                ret["code"] = 10002
                ret["msg"] = "用户token已过期, 请重新登录"
            else:
                ret["code"] = 10003
                ret["msg"] = "用户认证失败"
        else:
            ret["code"] = 10001
            ret["msg"] = "未传递用户认证请求头"
        # raise exceptions.AuthenticationFailed(ret)
        raise exceptions.NotAuthenticated(ret)

    def authenticate_header(self, request):
        pass

JWT

JWT 介绍

jwt 全称 json web token, 其实也是token的一种, 只是jwt是由header、payload、signature 三段组成的token, 格式为 header.payload.signature

jwt组成部分
  1. header 头(基础信息,也可以为空):加密方式、公司信息、项目组信息
  2. payload 载荷(核心信息):用户信息、过期时间、...(非敏感信息)
  3. signature 签名(安全保障):头加密结果+载荷加密结果+服务器秘钥 的md5加密结
jwt验证流程
  1. 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
  2. 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
  3. 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
  4. 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。

一个简单的jwt使用示例:

用户登录视图
class UserLoginJwtView(APIView):
    """
        通过jwt认证的方式登录,并返回jwt token
    """
    authentication_classes = []
    permission_classes = []
    def post(self, request, *args, **kwargs):
        rsp = {"code": 10000, "msg": "success"}
        user = UserLoginJwtSerializer(data=request.data)
        user.is_valid(raise_exception=True)
        token = user.token
        rsp["token"] = token
        return Response(rsp)
登录使用的serializer
class UserLoginJwtSerializer(serializers.ModelSerializer):
    """
        用户登录数据校验类, 校验成功并返回token (jwt 的方式)
    """
    mobile = serializers.CharField(write_only=True)
    password = serializers.CharField(write_only=True)

    class Meta:
        model = Users
        fields = ['mobile', 'password', 'username',  'email']
        extra_kwargs = {
            # username 好像是必须的, 而且用户model中的username 必须是唯一的
            'username': {
                'read_only': True
            },
            'email': {
                'read_only': True
            },
        }

    def validate(self, attrs):
        # 设置校验的数据
        mobile = attrs.get("mobile")
        password = attrs.get("password")
        
        # 这里判断了两种方式的登录, 手机号 和 邮箱
        if re.match(r'.*@.*', mobile):  # 匹配邮箱号 进行邮箱登录
            user_obj = Users.objects.filter(email=mobile).first()
        elif re.match(REGEX_MOBILE, mobile):  # 正则匹配手机号  进行手机登录
            user_obj = Users.objects.filter(mobile=mobile).first()  # 手机号登录
        else:
            raise serializers.ValidationError({"message":"用户手机号或邮箱格式错误"})
        
        if not user_obj:
            raise serializers.ValidationError({"message":"%s 未注册"%mobile})

        if user_obj and check_password(password, user_obj.password):
            payload = jwt_payload_handler(user_obj)
            token = jwt_encode_handler(payload)
            self.user = user_obj
            self.token = token
            return attrs
        raise serializers.ValidationError({"user":"参数错误"})

认证模块
class JWTAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        ret = {"code": 200, "msg": None}
        jwt_token = request.META.get('HTTP_AUTHORIZATION')

        if jwt_token is None:
            ret["code"] = 10001
            ret["msg"] = "未传递用户认证请求头, 禁止游客访问"
            # raise exceptions.AuthenticationFailed(ret)
            raise exceptions.NotAuthenticated(ret)

        # 自定义校验规则:auth token jwt
        token = self.parse_jwt_token(jwt_token)

        try:
            payload = jwt_decode_handler(token)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('token已过期')
        except:
            raise AuthenticationFailed('非法用户')
        user = self.authenticate_credentials(payload)

        return (user, token)

    #自定义校验规则:auth token jwt,auth为前盐,jwt为后盐
    def parse_jwt_token(self, jwt_token):
        tokens = jwt_token.split()
        if len(tokens) != 3 or tokens[0].lower() != 'bgcs' or tokens[2].lower() != 'jwt':
            return None
        return tokens[1]
JWT 配置过期时间
# 在settings.py 文件种添加一下配置
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
}

JWT 和 token 的对比

  1. 使用token方式, token必须存放在服务端的数据库里, 且每一次认证的时候都需要去查询一次数据, 会对数据库性能造成一点影响, 但能及时的管理用户token
  2. 而jwt 不需要服务端储存,用户登陆的信息关键存放在jwt加密数据,只需要在登录成功之后, 将jwt返回给客户端, 客户端请求数据的时候携带jwt即可, 认证函数在拿到jwt后只需要解密,解密成功后即认证成功, 对数据库服务器没有影响
  3. 由于jwt是无状态的设计, 所以一旦用户的jwt泄露, 服务端知道密文泄露也不能及时的注销被泄露的jwt密文, 为了应对这种情况, jwt内部有验证有效期和jwt黑名单模式,但是有效期始终无法做到及时停止jwt授权,而jwt黑名单模式,则需要数据库或内存存储黑名单, 这就违背了jwt的免数据库设计原则。
  4. jwt更适合安全级别较低的服务器设计,这种服务允许不严格的登陆授权,即使密文丢失也不会造成用户的严重损失,却能获得较高的服务器性能。