8-引入JWT完善注册接口

143 阅读11分钟

万字大章,请耐心看完!

介绍

JWT(JSON Web Token)是一种用于身份验证和授权的开放标准(RFC 7519),它可以在网络应用间传输声明。JWT 可以使用 HMAC 算法或 RSA 公钥/私钥对进行签名,以保证传输过程中的安全性。

一个 JWT 包括三个部分,分别是 Header、Payload 和 Signature。Header 包含了 JWT 的元信息,例如使用的算法;Payload 包含了要传输的声明信息,例如用户 ID、过期时间等;Signature 是对 Header 和 Payload 进行签名后得到的字符串,用于校验 JWT 的有效性。

使用 JWT 的好处在于它可以在不需要服务器端存储 session 或 token 的情况下完成身份验证和授权。客户端在登录后,可以将服务器返回的 JWT 存储在本地,之后的每次请求都将 JWT 放在请求头中进行传递,服务器可以通过校验 JWT 的有效性来判断请求是否合法。

当然,使用 JWT 也有一些注意事项。比如,JWT 中包含的信息可以被解码,但是无法防止信息泄露;JWT 的过期时间需要设置得合理,以避免 JWT 被长时间滥用;使用 JWT 时需要注意避免 XSS 和 CSRF 攻击等安全问题。

知识准备

  1. HTTPS:HTTPS 是一种安全的协议,可以保证在网络传输过程中数据的安全性和完整性。在使用 JWT 时,建议使用 HTTPS 协议来进行数据传输。
  2. 加密算法:在 JWT 中,用户信息需要被加密存储。因此,需要了解一些常见的加密算法,如 AES、RSA 等。
  3. 跨域问题:在使用 JWT 进行跨域请求时,需要了解跨域问题的解决方案,如 CORS、JSONP 等。
  4. JWT 的优缺点:了解 JWT 的优缺点有助于更好地评估其适用性以及在使用 JWT 时需要注意的问题。
  5. JWT 的使用场景:了解 JWT 的使用场景有助于更好地应用 JWT,避免出现不必要的问题。
  6. JWT 的安全性:了解 JWT 的安全性问题,有助于更好地保护用户信息,避免信息泄露和篡改等问题。
  7. Cookie是存储在用户浏览器中的小文件,其中包含了有关用户身份验证和应用程序状态的信息。当用户登录Web应用程序时,服务器会创建一个Cookie并将其发送回浏览器。浏览器会在以后的每个请求中发送该Cookie,以便服务器可以验证用户的身份并提供与其关联的授权。
  8. Token则是一种更为现代的身份验证机制,特别是在Web API中使用。使用Token,服务器会生成一个包含有关用户身份验证和授权的信息的JSON Web Token(JWT)。该Token包含加密的信息,可以在每个请求中发送到服务器以进行身份验证和授权。与Cookie不同,Token可以存储在任何客户端应用程序中,而不是仅限于浏览器。
  9. 加盐:JWT 中的 Token 是通过对数据进行签名来生成的,这个签名是使用一个密钥(也称作“加盐”)来计算的。在 JWT 中,通常使用 HMAC-SHA256 算法来计算签名。这个密钥只有服务器端知道,用来验证 Token 是否被篡改过。如果攻击者没有正确的密钥,就无法生成有效的签名,从而无法伪造有效的 Token。因此,加盐是 JWT 签名的一个重要部分,它可以保证 Token 的安全性。

难点

  1. 理解 JWT 的结构和编码方式:JWT 是由三部分组成的字符串,包括头部、荷载和签名,其中头部和荷载都是 JSON 格式的数据,并且需要使用 Base64 编码进行传输。而签名则需要使用密钥进行加密,以确保 JWT 的安全性。
  2. 选择适当的 JWT 库:由于 JWT 的实现比较复杂,因此很多编程语言都提供了相应的 JWT 库来简化开发者的工作。在选择 JWT 库时需要注意库的稳定性、安全性、功能性和使用方便性等方面的因素。
  3. 确定 JWT 的有效期和刷新机制:JWT 是有过期时间的,因此需要在编写代码时设定 JWT 的有效期,并且需要考虑刷新 JWT 的机制,以确保用户的登录状态不会过期。
  4. 处理 JWT 的错误和异常情况:在使用 JWT 过程中可能会出现一些错误和异常情况,比如 JWT 的签名验证失败、JWT 的过期时间已经到达等情况。因此,在编写 JWT 代码时需要考虑这些异常情况,并进行相应的处理,以确保 JWT 的正确性和安全性。

开始前须知

在编写 JWT 代码时需要充分理解 JWT 的结构和使用方法,并选择适当的 JWT 库。同时,还需要考虑 JWT 的有效期和刷新机制,并处理可能出现的错误和异常情况。所以,此次文章更新,会涉及到很多跟JWT相关的配置文件,跟json相关的处理方法,此次代码更新较多,请耐心观看!满满的全是干货!

选型

  • PyJWT

PyJWT 是一个用于编码和解码 JSON Web令牌(JWT)的 Python 库。与其他 JWT 库相比,PyJWT 具有易于使用、功能齐全、文档完善等优点。PyJWT 支持所有标准的 JWT 签名算法,包括 HMAC、RSA 和 ECDSA。此外,PyJWT 还支持 JSON Web Key(JWK)和 JSON Web Algorithms(JWA)。

  • jwt

jwt 模块是一个轻量级 JWT 库,它比 PyJWT 更简单,但功能也更少。jwt 模块只支持 HMAC 签名算法,并且不支持 JWK 和 JWA。

在此,我们选择的是PyJWT模块来编写JWT相关功能。

编写计划

  1. 在config中添加JWT相关配置
  2. 编写JWT相关工具类
  3. 编写encode相关工具类

编写

  1. 在config.py中添加以下配置
# JWT配置信息  
EXPIRED_HOUR: int = 1 # 失效时间,单位小时  
JWT_KEY: str = 'AbandonToken'  
JWT_SALT: str = 'Abandon'
  1. 新建abandon-server/src/app/middleware/my_jwt.py,编辑以下内容:
import hashlib  
import jwt  
from datetime import timedelta, datetime  
  
from config import AbandonConfig  
  
  
class AbandonJWT(object):  
  
    @staticmethod  
    def get_token(data):  
        """  
        定义一个静态方法 get_token,用于生成 JWT 令牌  
        :param data:  
        :return: 过期时间和生成的 JWT 令牌  
        """  
        # 计算过期时间  
        expire = datetime.now() + timedelta(hours=AbandonConfig.EXPIRED_HOUR)  
        # 将过期时间加入到输入数据中,并生成新的数据  
        new_data = dict({"exp": datetime.utcnow() + timedelta(hours=AbandonConfig.EXPIRED_HOUR)}, **data)  
        return int(expire.timestamp()), jwt.encode(new_data, key=AbandonConfig.JWT_KEY, algorithm='HS256')  

    @staticmethod  
    def parse_token(token):  
        """  
        定义一个静态方法 parse_token,用于解析 JWT 令牌  
        :param token:  
        :return: 使用密钥解码 JWT 令牌,并返回解码后的数据  
        """  
        try:  
            return jwt.decode(token, key=AbandonConfig.JWT_KEY, algorithms=["HS256"])  
        except Exception:  
            raise Exception("登录状态校验失败, 请重新登录")  

    @staticmethod  
    def add_salt(password):  
        """  
        定义一个静态方法 add_salt,用于给密码添加盐并进行 MD5 加密  
        :param password:  
        :return: 加密结果的十六进制  
        """  
        # 创建一个 MD5 对象  
        m = hashlib.md5()  
        # 将密码和盐拼接,并将字符串转换为字节串  
        bt = f"{password}{AbandonConfig.JWT_SALT}".encode("utf-8")  
        m.update(bt)  
        return m.hexdigest()  
  
  
if __name__ == '__main__':  
    expire, token = AbandonJWT.get_token({'这是一个usr': 1})  
    print(expire, token)  
    print(AbandonJWT.parse_token(token))

main函数中是自我测试使用,原理也很简单,把data传入进去,打印一下生成的token信息,再根据密钥解密,确保此JWT工具类的加解密正常使用。注释已经打满了,就是为了方便没有接触过JWT的同学理解使用。

  1. 编写一些用于将 Python 对象转换为 JSON 格式的函数和类。新建abandon-server/src/app/middleware/encoder.py
import dataclasses
import json
from collections import defaultdict
from datetime import datetime
from decimal import Decimal
from enum import Enum
from pathlib import PurePath
from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union  # 导入各种类型

from pydantic import BaseModel  # 导入 BaseModel 类
from pydantic.json import ENCODERS_BY_TYPE

SetIntStr = Set[Union[int, str]]  # 定义 SetIntStr 类型为 int 和 str 的集合
DictIntStrAny = Dict[Union[int, str], Any]  # 定义 DictIntStrAny 类型为 int 或 str 作为键,任意类型作为值的字典
TupleIntStr = Tuple[str]  # 定义 TupleIntStr 类型为只包含字符串的元组


class JsonEncoder(json.JSONEncoder):  # 定义 JsonEncoder 类,继承自 json.JSONEncoder 类

    def default(self, o: Any) -> Any:  # 定义 default 方法,接受一个任意类型的参数 o,返回一个任意类型的结果
        if isinstance(o, set):  # 如果 o 是 set 类型
            return list(o)  # 将其转换为列表类型并返回
        if isinstance(o, datetime):  # 如果 o 是 datetime 类型
            return str(int(o.timestamp()))  # 返回十位时间戳(将 datetime 类型转换为字符串类型)
            # return o.strftime("%Y-%m-%d %H:%M:%S")  # 返回格式化的时间字符串
        if isinstance(o, Decimal):  # 如果 o 是 Decimal 类型
            return str(o)  # 将其转换为字符串类型并返回
        if isinstance(o, bytes):  # 如果 o 是 bytes 类型
            return o.decode(encoding='utf-8')  # 将其转换为字符串类型并返回

        return self.default(o)  # 否则递归调用 default 方法


def generate_encoders_by_class_tuples(  # 定义 generate_encoders_by_class_tuples 函数,接受一个字典类型的参数 type_encoder_map,返回一个字典类型的结果
        type_encoder_map: Dict[Any, Callable[[Any], Any]]
) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]:
    encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(
        tuple
    )  # 定义 encoders_by_class_tuples 变量,初始化为空 defaultdict 对象
    for type_, encoder in type_encoder_map.items():  # 遍历 type_encoder_map 中的每一项
        encoders_by_class_tuples[encoder] += (type_,)  # 将 encoder 添加到 encoders_by_class_tuples 中对应类型的元组中
    return encoders_by_class_tuples  # 返回 encoders_by_class_tuples 变量


encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)  # 调用 generate_encoders_by_class_tuples 函数,将 ENCODERS_BY_TYPE 变量作为参数,并将结果赋值给 encoders_by_class_tuples 变量


def jsonable_encoder(  # 定义 jsonable_encoder 函数,接受一个任意类型的参数 obj,以及一些可选参数,返回一个任意类型的结果
        obj: Any,
        include: Optional[Union[SetIntStr, DictIntStrAny, TupleIntStr]] = None,
        exclude: Optional[Union[SetIntStr, DictIntStrAny, TupleIntStr]] = None,
        by_alias: bool = True,
        exclude_unset: bool = False,
        exclude_defaults: bool = False,
        exclude_none: bool = False,
        custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None,
        sqlalchemy_safe: bool = True,
) -> Any:
    custom_encoder = custom_encoder or {}  # 如果 custom_encoder 是 None,就将其设为一个空字典
    if custom_encoder:  # 如果 custom_encoder 不为空
        if type(obj) in custom_encoder:  # 如果 obj 的类型在 custom_encoder 中
            return custom_encoder[type(obj)](obj)  # 调用 custom_encoder 中对应类型的函数并将 obj 作为参数,返回结果
        else:  # 否则
            for encoder_type, encoder_instance in custom_encoder.items():  # 遍历 custom_encoder 中的每一项
                if isinstance(obj, encoder_type):  # 如果 obj 是 encoder_type 类型的实例
                    return encoder_instance(obj)  # 调用 encoder_instance 函数并将 obj 作为参数,返回结果
    if include is not None and not isinstance(include, (set, dict)):  # 如果 include 不为空且不是集合或字典类型
        include = set(include)  # 将其转换为集合类型
    if exclude is not None and not isinstance(exclude, (set, dict)):  # 如果 exclude 不为空且不是集合或字典类型
        exclude = set(exclude)  # 将其转换为集合类型
    if isinstance(obj, BaseModel):  # 如果 obj 是 BaseModel 的实例
        encoder = getattr(obj.__config__, "json_encoders", {})  # 从 obj.__config__.json_encoders 中获取 encoder 变量,如果没有则设为一个空字典
        if custom_encoder:  # 如果 custom_encoder 不为空
            encoder.update(custom_encoder)  # 将 custom_encoder 添加到 encoder 中
        obj_dict = obj.dict(  # 将 obj 转换为字典类型
            include=include,  # 包含哪些键
            exclude=exclude,  # 排除哪些键
            by_alias=by_alias,  # 是否使用别名
            exclude_unset=exclude_unset,  # 是否排除未设置的键
            exclude_none=exclude_none,  # 是否排除值为 None 的键
            exclude_defaults=exclude_defaults,  # 是否排除默认值的键
        )
        if "__root__" in obj_dict:  # 如果 "__root__" 在 obj_dict 中
            obj_dict = obj_dict["__root__"]  # 则将其赋值给 obj_dict
        return jsonable_encoder(  # 递归调用 jsonable_encoder 函数
            obj_dict,  # 参数为 obj_dict
            exclude_none=exclude_none,
            exclude_defaults=exclude_defaults,
            custom_encoder=encoder,
            sqlalchemy_safe=sqlalchemy_safe,
        )
    if dataclasses.is_dataclass(obj):  # 如果 obj 是 dataclass 的实例
        return dataclasses.asdict(obj)  # 将其转换为字典类型并返回
    if isinstance(obj, Enum):  # 如果 obj 是 Enum 的实例
        return obj.value  # 返回 obj 的 value 属性
    if isinstance(obj, PurePath):  # 如果 obj 是 PurePath 的实例
        return str(obj)  # 将其转换为字符串类型并返回
    if isinstance(obj, (str, int, float, type(None))):  # 如果 obj 是字符串、整数、浮点数或 None 类型
        return obj  # 直接返回 obj
    if isinstance(obj, dict):  # 如果 obj 是字典类型
        encoded_dict = {}  # 定义 encoded_dict 变量,初始化为空字典
        for key, value in obj.items():  # 遍历 obj 中的每一项
            if (
                    (
                            not sqlalchemy_safe  # 如果 sqlalchemy_safe 为 False
                            or (not isinstance(key, str))  # 或者 key 不是字符串类型
                            or (not key.startswith("_sa"))  # 或者 key 不是以 "_sa" 开头的字符串
                    )
                    and (value is not None or not exclude_none)  # 并且 value 不是 None 或者 exclude_none 为 False
                    and ((include and key in include) or not exclude or key not in exclude)  # 并且 key 包含在 include 中或者 exclude 为 None 或者 key 不在 exclude 中
            ):
                encoded_key = jsonable_encoder(  # 将 key 转换为 JSON 格式
                    key,
                    exclude=exclude,
                    by_alias=by_alias,
                    exclude_unset=exclude_unset,
                    exclude_none=exclude_none,
                    custom_encoder=custom_encoder,
                    sqlalchemy_safe=sqlalchemy_safe,
                )
                encoded_value = jsonable_encoder(  # 将 value 转换为 JSON 格式
                    value,
                    by_alias=by_alias,
                    exclude=exclude,
                    exclude_unset=exclude_unset,
                    exclude_none=exclude_none,
                    custom_encoder=custom_encoder,
                    sqlalchemy_safe=sqlalchemy_safe,
                )
                encoded_dict[encoded_key] = encoded_value  # 将转换后的键值对添加到 encoded_dict 中
        return encoded_dict  # 返回 encoded_dict 变量
    if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):  # 如果 obj 是列表、集合、冻结集合、生成器或元组类型
        encoded_list = []  # 定义 encoded_list 变量,初始化为空列表
        for item in obj:  # 遍历 obj 中的每一项
            encoded_list.append(  # 将转换后的项添加到 encoded_list 中
                jsonable_encoder(
                    item,
                    include=include,
                    exclude=exclude,
                    by_alias=by_alias,
                    exclude_unset=exclude_unset,
                    exclude_defaults=exclude_defaults,
                    exclude_none=exclude_none,
                    custom_encoder=custom_encoder,
                    sqlalchemy_safe=sqlalchemy_safe,
                )
            )
        return encoded_list  # 返回 encoded_list 变量

    if type(obj) in ENCODERS_BY_TYPE:  # 如果 obj 的类型在 ENCODERS_BY_TYPE 中
        return ENCODERS_BY_TYPE[type(obj)](obj)  # 调用 ENCODERS_BY_TYPE 中对应类型的函数并将 obj 作为参数,返回结果
    for encoder, classes_tuple in encoders_by_class_tuples.items():  # 遍历 encoders_by_class_tuples 中的每一项
        if isinstance(obj, classes_tuple):  # 如果 obj 是 classes_tuple 中的任意一种类型的实例
            return encoder(obj)  # 调用 encoder
  1. 现在我们只需要在register接口中调用部分更改后的功能即可,代码如下:、
# router注册的函数都会自带/auth,所以url是/auth/register  
@router.post("/register")  
async def register():  
    usr_info: dict = {'这是一个usr': 1}  
    expire, token = AbandonJWT.get_token(usr_info)  
    return AbandonJSONResponse.success(dict(token=token, expire=expire, usr_info=usr_info))

验证

使用Postman请求127.0.0.1:9923/auth/register地址查看返回即可。

image.png