python Django 之 DRF(六)jwt的源码分析、结合认证类使用


前言

对于前后端分离的项目中,我们通常会在数据库中给用户表设计一个token的字段,是一种判别用户的手段,但如果当用户数据量大的时候,我们的数据库将存放很多用户token,并且每次登录的时候都会进行数据库查询。

如果我们还想让用户登录完成后的token设置超时时间,那么数据库的字段又需要加一列,显然是非常不友好的,为了解决这个问题,jwt的作用就诞生了。

jwt官网地址:https://jwt.io/introduction

通过python的方式使用jwt需安装pyjwt如下:

pip install pyjwt

一、jwt认证流程、原理

jwt(JSON Web Tokens),是一种开发的行业标准 RFC 7519 ,用于安全的表示双方之间的声明。目前,jwt广泛应用在系统的用户认证方面,特别是现在前后端分离项目

那么对于jwt而言是怎么通过内部自实现一个token呢?

基于jwt实现token如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

可以看到通过jwt生成的token是由三段字符串组成,并且用.连接起来,而这三段字符串是通过什么样的加密方式演算出来的呢?我们将逐一解析。

  • 对于第一段字符串,我们将其密钥称为HEADER(算法与令牌类型)是一个字典,其内部包含了使用算法默认为HS256(指哈希消息认证码,包含有很多种哈希加密算法,HS256是其中一种),以及token加密方式默认为JWT。
{
  "alg": "HS256",
  "typ": "JWT"
}

那么对于使用JWT的方式进行token加密,其内部是通过json转化成字符串,然后做base64url加密(base64加密 + 替换),那么此时第一段字符串的加密就完成了。

  • 对于第二段字符串,我们将其密钥称为PAYLOAD(自定义加密数据),并且还可以设置token有效时间。
{
  "sub": "1234567890",
  "name": "John Doe",
  "exp": 1516239022 # 超时时间
}

而其加密过程和第一段字符串一样使用了base64url加密,但是这里要注意,因为该加密方式是通过base64加密 + 替换操作实现的,本质上是可以解密的,所以对于在实际应用场景中,不得将用户敏感数据传入

  • 对于第三段字符串而言,就有点复杂了该加密方式通过第一、二生成的密文进行拼接,并对拼接完成的密文进行HS256加密并将服务器唯一私钥当成密钥放入,之后在进行一次base64url加密。

那么对于用户携带token后端进行token校验的流程如下:

  1. 对token进行切割(以.的方式)
  2. 对第二段进行base64url解密,并获取payload信息(通过参数exp判断token1是否已超时)
  3. 将第1、2段的密文拼接,再次执行sha256加密之后将服务器唯一私钥放入在和之前的加密token进行比较,如果相等表示token未被修改认证通过

那么明白流程后,我们就通过DRF的认证类来使用吧。

二、通过jwt实战DRF认证

这里我们为了方便测试通过sqllite3创建用户表,表结构如下:

姓名密码
测试员666666

models.py如下:

class UserTest(models.Model):
    password = models.CharField(verbose_name="密码", max_length=32)
    username = models.CharField(verbose_name='姓名', max_length=64)

python manage.py makemigrations -----生成迁移文件

python manage.py migrate -----执行迁移命令

urls.py如下:

from django.conf.urls import url
from api import views

urlpatterns = [

    url(r'^jwt_login/$', views.JwtLoginView.as_view()),
    url(r'^jwt_order/$', views.JwtOrderView.as_view()),
]

认证类如下:

from rest_framework.authentication import BaseAuthentication
from api import models
from rest_framework.exceptions import AuthenticationFailed
from django.conf import settings
import jwt
from jwt import exceptions

class JwtAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.META.get('HTTP_AUTHORIZATION', None)
        # 如果没有获取到token(即未登录状态)
        if not token:
            raise AuthenticationFailed()

        salt = settings.SECRET_KEY
        # 1.切割
        # 2.解密第二段/判断过期
        # 3.验证第三段合法性
        try:

            payload = jwt.decode(token, salt, "HS256")
        except exceptions.ExpiredSignatureError:

            raise AuthenticationFailed({'code': 1003, "error": "token已失效"})

        except jwt.DecodeError:
            raise AuthenticationFailed({'code': 1003, "error": "token认证失败"})
        except jwt.InvalidTokenError:
            raise AuthenticationFailed({'code': 1003, "error": "非法的token"})
        return (payload, token)

此时在视图上authentication_classes继承该类即可实现通过在请求头AUTHORIZATION中获取到的token进行判断。

认证相关的完成后,就要到通过jwt生成token,我们将其封装成一个函数create_token并放到encrypt.py中。

函数如下:

import datetime
import jwt
from django.conf import settings

def create_token(payload, timeout=1):
    salt = settings.SECRET_KEY
    # 构造headr
    headers = {
        'typ': 'jwt',
        'alg': 'HS256'
    }
    # 构造payload
    payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
    # 加密
    token = jwt.encode(payload=payload, key=salt, algorithm="HS256", headers=headers)
    return token

该函数有两个传参,timeout为超时时间(以分钟为单位默认为1分钟),payload为之前讲过token加密参数中的第二段字符串中加密需要的参数(结合timeout可以给token设置过期时间),而headers即为第一段字符串中的加密方式(这里选择默认方式),最后在传入服务器端中的私钥通过HS256的加密方式进行,返回结果给token。

views.py如下:

from rest_framework.views import APIView
from api.authentication.auth import JwtAuthentication
from api import models
from rest_framework.response import Response
from utils.encrypt import create_token

# 用户登录页
class JwtLoginView(APIView):
    def post(self, request, *args, **kwargs):
        user = request.data.get('username')
        pwd = request.data.get('password')
        user_object = models.UserTest.objects.filter(username=user, password=pwd).first()
        if not user_object:
            return Response({'code': 1000, "error": "用户名或密码错误"})
        token = create_token({'id': getattr(user_object, 'id', None), 'name': getattr(user_object, "username", None)})
        return Response({'code': 1001, "data": token})
        # 登录才能访问


class JwtOrderView(APIView):
    authentication_classes = [JwtAuthentication]

    def get(self, request, *args, **kwargs):
        print(request.user, request.auth)
        return Response('订单列表')

这里用了一个知识点通过getattr(“对象”,“字段”,“默认”)也等价于对象.字段。

此时我们通过postman进行测试如下:
在这里插入图片描述

可以看到通过jwt认证其token是由三部分组成的(两个.分割),并且token正确也能通过,token错误、超时认证类也会进行处理,此时的jwt结合DRF认证类将更加强大、安全、效率上也有所提高。

三、jwt校验token源码分析

在上述使用jwt进行token加密、解密中是否按照着我们之前分析的那种情况来的呢?我们现在通过阅读源码的形式进行分析。

在前面中写的认证类解密、登录时加密token的封装其内部都调用了一个jwt的decode、encode函数,并需传多个参数,这里就讲常用的参数。

  • decode(参数:“服务端返回给用户的真实token”,“服务端私钥”,“加密方式”)该函数将token进行解密
  • encode(参数:“用户返回数据(超时时间)”,“服务端私钥”,“加密方式”,“第一次字符串加密参数headers”)该函数将会生成token

从生成token的函数encode进行源码分析如下:

    def encode(
        self,
        payload: Dict[str, Any],
        key: str,
        algorithm: str = "HS256",
        headers: Optional[Dict] = None,
        json_encoder: Optional[Type[json.JSONEncoder]] = None,
    ) -> str:
        # Check that we get a mapping
        if not isinstance(payload, Mapping):
            raise TypeError(
                "Expecting a mapping object, as JWT only supports "
                "JSON objects as payloads."
            )

        # Payload
        payload = payload.copy()
        for time_claim in ["exp", "iat", "nbf"]:
            # Convert datetime to a intDate value in known time-format claims
            if isinstance(payload.get(time_claim), datetime):
                payload[time_claim] = timegm(payload[time_claim].utctimetuple())

        json_payload = json.dumps(
            payload, separators=(",", ":"), cls=json_encoder
        ).encode("utf-8")

        return api_jws.encode(json_payload, key, algorithm, headers, json_encoder)

前面传参数的不管,我们看后面,该函数通过payload.copy()继续了浅拷贝,然后遍历一个数组(内部会帮我们实现token过期时间)通过判断time_claim的值执行相应函数。而后面的操作就相当于我们前面说过将数据转化为json格式然后进行替换操作了。最后调用了api_jws.encode将替换好的值传入,此时我们访问一下该函数如下:

    def encode(
        self,
        payload: bytes,
        key: str,
        algorithm: str = "HS256",
        headers: Optional[Dict] = None,
        json_encoder: Optional[Type[json.JSONEncoder]] = None,
    ) -> str:
        segments = []

        if algorithm is None:
            algorithm = "none"

        if algorithm not in self._valid_algs:
            pass

        # Header
        header = {"typ": self.header_typ, "alg": algorithm}

        if headers:
            self._validate_headers(headers)
            header.update(headers)

        json_header = json.dumps(
            header, separators=(",", ":"), cls=json_encoder
        ).encode()

        segments.append(base64url_encode(json_header))
        segments.append(base64url_encode(payload))

        # Segments
        signing_input = b".".join(segments)
        try:
            alg_obj = self._algorithms[algorithm]
            key = alg_obj.prepare_key(key)
            signature = alg_obj.sign(signing_input, key)

        except KeyError:
            if not has_crypto and algorithm in requires_cryptography:
                raise NotImplementedError(
                    "Algorithm '%s' could not be found. Do you have cryptography "
                    "installed?" % algorithm
                )
            else:
                raise NotImplementedError("Algorithm not supported")

        segments.append(base64url_encode(signature))

        encoded_string = b".".join(segments)

        return encoded_string.decode("utf-8")

我们可以看到该函数给header赋了默认值(内部"JWT"和"HS256"),然后判断了一下我们的headers是否存在,如果存在就用我们的headers,如果不存在就用默认的,所以对于haders我们可传可不传,之后又继续替换操作然后将值进行头部headers(即第一段字符串加密操作)的base64url_encode加密,并存放到segments列表中(一开始为空列表),然后继续将payload(即第二段字符串加密操作)做同样的base64url_encode加密,将这两个加密的数据放入一个列表中。

之后用.进行拼接并返回给signing_input,然后到try中寻找该参数传入的加密算法,将key提高prepare_key函数获取到(key为服务端的私钥,内部就是进行key是否存在的判断)。

 def prepare_key(self, key):
        if key == "":
            key = None

        if key is not None:
            raise InvalidKeyError('When alg = "none", key value must be None.')

        return key

之后通过加密对象(默认情况下为hs256对象)调用sign函数。

HMACAlgorithm类/sign函数如下:

import hmac
class HMACAlgorithm(Algorithm):
    """
    Performs signing and verification operations using HMAC
    and the specified hash function.
    """

    SHA256 = hashlib.sha256
    SHA384 = hashlib.sha384
    SHA512 = hashlib.sha512

    def __init__(self, hash_alg):
        self.hash_alg = hash_alg

    def sign(self, msg, key):
        return hmac.new(key, msg, self.hash_alg).digest()

抛开其他函数,我们看sign函数已经不能往下走了,其内部通过hmac库中的算法创建一个sha256加密结果并返回(此时第三段字符已经完成sha256加密)赋值给了signature ,然后再进行一次base64url加密放入segments列表中最后将这3个加密字符串通过.的形式拼接起来并返回。

可以看出通过源码的分析发现jwt中encode加密函数和我们上述描写的思路是一致的。

那么解密decode的源码如下:

    def decode(
        self,
        jwt: str,
        key: str = "",
        algorithms: List[str] = None,
        options: Dict = None,
        **kwargs,
    ) -> str:
        decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs)
        return decoded["payload"]

可以看到decode函数调用了decode_complete函数,我们点进去查看:

    def decode_complete(
        self,
        jwt: str,
        key: str = "",
        algorithms: List[str] = None,
        options: Dict = None,
        **kwargs,
    ) -> Dict[str, Any]:
        if options is None:
            options = {}
        merged_options = {**self.options, **options}
        verify_signature = merged_options["verify_signature"]

        if verify_signature and not algorithms:
            raise DecodeError(
                'It is required that you pass in a value for the "algorithms" argument when calling decode().'
            )

        payload, signing_input, header, signature = self._load(jwt)

        if verify_signature:
            self._verify_signature(signing_input, header, signature, key, algorithms)

        return {
            "payload": payload,
            "header": header,
            "signature": signature,
        }

前面参数不看,我们直接看decode_complete函数调用了_load函数,还有一个jwt参数(即用户token),此时我们点进去查看:

    def _load(self, jwt):
        if isinstance(jwt, str):
            jwt = jwt.encode("utf-8")

        if not isinstance(jwt, bytes):
            raise DecodeError(f"Invalid token type. Token must be a {bytes}")

        try:
            signing_input, crypto_segment = jwt.rsplit(b".", 1)
            header_segment, payload_segment = signing_input.split(b".", 1)
        except ValueError as err:
            raise DecodeError("Not enough segments") from err

        try:
            header_data = base64url_decode(header_segment)
        except (TypeError, binascii.Error) as err:
            raise DecodeError("Invalid header padding") from err

        try:
            header = json.loads(header_data)
        except ValueError as e:
            raise DecodeError("Invalid header string: %s" % e) from e

        if not isinstance(header, Mapping):
            raise DecodeError("Invalid header string: must be a json object")

        try:
            payload = base64url_decode(payload_segment)
        except (TypeError, binascii.Error) as err:
            raise DecodeError("Invalid payload padding") from err

        try:
            signature = base64url_decode(crypto_segment)
        except (TypeError, binascii.Error) as err:
            raise DecodeError("Invalid crypto padding") from err

        return (payload, signing_input, header, signature)

类型判断我们直接跳过,看try中该函数通过rsplit函数将数据通过.的形式分割成两段(第一、二段加密字符串为一段,第三段加密字符串为第二段),然后将signing_input(一、二段加密字符串)再分割,使其将3段加密字符串放到不同的变量中,然后依次解密,最后返回解密的结果。

返回后,继续往下走调用_verify_signature函数,其需要参数为base64url解密完成后和加密的方式,函数源码如下:

    def _verify_signature(
        self,
        signing_input,
        header,
        signature,
        key="",
        algorithms=None,
    ):

        alg = header.get("alg")

        if algorithms is not None and alg not in algorithms:
            raise InvalidAlgorithmError("The specified alg value is not allowed")

        try:
            alg_obj = self._algorithms[alg]
            key = alg_obj.prepare_key(key)

            if not alg_obj.verify(signing_input, key, signature):
                raise InvalidSignatureError("Signature verification failed")

        except KeyError:
            raise InvalidAlgorithmError("Algorithm not supported")

该函数通过alg获取到加密方式(默认hs256),然后通过_algorithms函数获取加密对象(默认hs256对象),再获取服务器唯一密钥后,通过加密对象.verify方法并把参数全部传入进行验证(判断token是否和当前用户传的一致)。

HMACAlgorithm类/verify函数如下:

import hmac
class HMACAlgorithm(Algorithm):
    """
    Performs signing and verification operations using HMAC
    and the specified hash function.
    """

    SHA256 = hashlib.sha256
    SHA384 = hashlib.sha384
    SHA512 = hashlib.sha512

    def __init__(self, hash_alg):
        self.hash_alg = hash_alg
        
    def verify(self, msg, key, sig):
        return hmac.compare_digest(sig, self.sign(msg, key))

抛开其他函数我们只看verify函数,走到这里就不走了,此时verify函数会调用hmac算法进行比较,最后将结果返回。

借鉴hamc中比较的思路我们可以发现通过jwt中decode函数源码分析,也和我们上述描写的概念是一样的。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值