PyJWT 和 python-jose 在处理JWT令牌处理的时候的差异和具体使用

PyJWTpython-jose 是两个用于处理 JSON Web Tokens (JWT) 的 Python 库。它们都有助于生成、解码、验证和管理 JWT,但它们在功能范围和设计哲学上有一些重要的区别。本篇介绍它们之间的一些差异,以及在项目中使用FastAPI+ python-jose 来处理访问令牌的生成以及一些例子代码供参考。

1、PyJWTpython-jose的差异

PyJWT

PyJWT 是一个专门处理 JWT 的 Python 库,它旨在简化 JWT 的创建和验证。

特点:

  • 专注于 JWT: PyJWT 专门用于 JWT 的处理,不提供其他类型的加密或签名功能。
  • 简单易用: PyJWT 提供了简单的 API,用于创建和验证 JWT。
  • 支持常见的签名算法: 包括 HMAC (HS256, HS384, HS512) 和 RSA (RS256, RS384, RS512)。
  • 轻量级: 由于 PyJWT 专注于 JWT,依赖少,安装和使用都很简便。

主要用法示例:

复制代码
import jwt
import datetime

# 创建一个JWT
payload = {
    "user_id": 123,
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
secret = 'your-secret-key'
token = jwt.encode(payload, secret, algorithm='HS256')

# 解码JWT
decoded = jwt.decode(token, secret, algorithms=['HS256'])
print(decoded)

python-jose

python-jose 是一个更广泛的加密库,它不仅支持 JWT,还支持多种 JOSE (JSON Object Signing and Encryption) 标准,包括 JWS (JSON Web Signature)、JWE (JSON Web Encryption)、JWK (JSON Web Key)、JWA (JSON Web Algorithms) 等。

特点:

  • 全面的 JOSE 支持 : 除了 JWT,python-jose 还支持其他 JOSE 标准,因此功能更强大、更灵活。
  • 多种加密与签名算法: 支持比 PyJWT 更多的算法,如直接加密 (dir)、对称密钥加密 (A256KW, A192KW, A128KW),以及 RSA 和 ECDSA 的多种模式。
  • 丰富的功能: 提供更细粒度的控制,适合需要复杂加密和签名操作的应用场景。
  • 相对复杂 : 由于功能更广泛,python-jose 的使用复杂度比 PyJWT 更高。

主要用法示例:

复制代码
from jose import jwt
from jose.exceptions import JWTError

# 创建一个JWT
payload = {
    "user_id": 123,
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
secret = 'your-secret-key'
token = jwt.encode(payload, secret, algorithm='HS256')

# 解码JWT
try:
    decoded = jwt.decode(token, secret, algorithms=['HS256'])
    print(decoded)
except JWTError as e:
    print(f"Token is invalid: {e}")

差异总结

  • 功能范围 : PyJWT 专注于 JWT,适合需要简单 JWT 处理的项目;python-jose 则支持整个 JOSE 标准,适合需要更复杂加密和签名操作的项目。
  • 易用性 : PyJWT API 简单,易于上手;python-jose 更强大,但同时也更复杂。
  • 算法支持 : python-jose 支持的算法更广泛,尤其是在需要高级加密或签名场景时更具优势。
  • 使用场景 : 如果你的项目只需要生成和验证 JWT,PyJWT 是一个不错的选择;如果你需要全面的 JOSE 支持,包括 JWS、JWE 等,或者需要复杂的加密和签名,python-jose 是更好的选择。

2、使用 python-jose 处理 JWT

在使用 python-jose 处理 JWT 时,捕获和处理异常是一个重要的环节。

1) 安装 python-jose

首先,确保你已经安装了 python-jose

复制代码
pip install python-jose

2)使用 python-jose 的 JWT 模块

以下是一个使用 python-jose 的 JWT 处理的示例,包括如何捕获异常:

复制代码
from jose import jwt, JWTError
from jose.exceptions import ExpiredSignatureError

# 定义密钥和有效负载
secret = 'your-secret-key'
payload = {
    "user_id": 123,
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}

# 生成 JWT
token = jwt.encode(payload, secret, algorithm='HS256')

# 解码 JWT 并处理可能的异常
try:
    decoded = jwt.decode(token, secret, algorithms=['HS256'])
    print(decoded)
except ExpiredSignatureError:
    print("Token has expired")
except JWTError:
    print("Token is invalid")

在处理 python-jose 的 JWT 时,正确地捕获和处理异常是关键。确保你的环境和工具能够正确识别异常类型,将有助于你更好地管理 JWT 错误。

如果我们需要再JWT的playload里面承载更多的信息,可以再claim中声明键值即可,你可以使用 python-joseFastAPI 来实现 JWT 令牌生成操作。如下所示代码。

使用 FastAPIpython-jose 生成 JWT 令牌:

复制代码
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.responses import JSONResponse
from jose import JWTError, jwt
from datetime import datetime, timedelta

app = FastAPI()

# 配置项,通常从配置文件中加载
JWT_SECRET_KEY = 'your_jwt_secret_key'
JWT_ISSUER = 'your_issuer'
JWT_AUDIENCE = 'your_audience'
JWT_EXPIRED_DAYS = 7
ALGORITHM = 'HS256'

def generate_token(user_info: dict, role_type: str):
    # 获取IP地址
    ip = user_info.get('ip', '')

    # 定义声明
    claims = {
        'id': user_info['id'],
        'email': user_info['email'],
        'name': user_info['name'],
        'nickname': user_info.get('nickname', ''),
        'phone_number': user_info.get('mobile_phone', ''),
        'gender': user_info.get('gender', ''),
        'full_name': user_info.get('full_name', ''),
        'company_id': user_info.get('company_id', ''),
        'company_name': user_info.get('company_name', ''),
        'dept_id': user_info.get('dept_id', ''),
        'dept_name': user_info.get('dept_name', ''),
        'role_type': role_type,
        'ip': ip,
        'mac_addr': '',  # 无法获得Mac地址
        'channel': ''
    }

    # 定义token过期时间
    expiration = datetime.utcnow() + timedelta(days=JWT_EXPIRED_DAYS)

    # 创建JWT token
    to_encode = {
        **claims,
        'iss': JWT_ISSUER,
        'aud': JWT_AUDIENCE,
        'exp': expiration
    }

    token = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=ALGORITHM)
    return token
复制代码
@app.post('/token')
async def get_token(request: Request):
    # 模拟的用户信息
    user_info = {
        'id': 123,
        'email': 'user@example.com',
        'name': 'John Doe',
        'nickname': 'Johnny',
        'mobile_phone': '123-456-7890',
        'gender': 'Male',
        'full_name': 'Johnathan Doe',
        'company_id': 'ABC123',
        'company_name': 'ABC Corp',
        'dept_id': 'Dept001',
        'dept_name': 'IT',
        'ip': request.client.host
    }
    role_type = 'Admin'

    token = generate_token(user_info, role_type)
    
    headers = {
        'access-token': token,
        'Authorization': f'Bearer {token}'
    }

    return JSONResponse(content={'token': token}, headers=headers)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

解释

  1. FastAPI: 用于构建快速、现代的 Web API。
  2. Request : 从 FastAPI 中导入,用于获取客户端的 IP 地址。
  3. JWT 配置: 使用常量配置 JWT 密钥、发行者、受众、加密算法和过期时间。
  4. generate_token 函数 :
    • 构建 JWT 的 claims,包含用户信息和额外的字段,如 IP 地址、角色类型等。
    • 设置 exp 字段定义 JWT 的过期时间。
    • 使用 jwt.encode 创建并签名 JWT。
  5. get_token 路由 :
    • 模拟从请求中获取用户信息(包括 IP 地址)。
    • 调用 generate_token 生成 JWT。
    • 将 JWT 放入响应头中,返回给客户端。

3、在api中如何实现AllowAnonymous和验证授权

在 Python 的 FastAPI 框架中,你可以通过以下方式实现类似于 ASP.NET 中的 AllowAnonymous 和授权验证功能。

1)实现 JWT 授权验证中间件

首先,你需要一个依赖项来检查请求中是否包含有效的 JWT 令牌。如果令牌无效或缺失,依赖项将拒绝请求。通过这种方式,只有标记为"允许匿名"的路由才会跳过验证。

2)安装依赖

确保安装了 python-jose 用于处理 JWT,以及 fastapi

3) 创建授权依赖

你可以创建一个名为 get_current_user 的依赖项,用于验证 JWT 令牌并提取用户信息。如果没有提供或验证失败,抛出 HTTPException

复制代码
from fastapi import Depends, HTTPException, status
from jose import JWTError, jwt
from typing import Optional

JWT_SECRET_KEY = 'your_jwt_secret_key'
ALGORITHM = 'HS256'

def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("id")
        if user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Could not validate credentials",
                headers={"WWW-Authenticate": "Bearer"},
            )
        return payload
    except JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Could not validate credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )

get_current_user :用于解析和验证 JWT。如果令牌无效或缺失,会抛出 HTTPException,返回 401 状态码。

具体使用的时候,我们可能把用户信息缓存在Redis里面提高处理效率。

4)实现 AllowAnonymous 功能

FastAPI 中,你可以通过 Depends 来实现条件授权。对于需要授权的路由,只需将 get_current_user 作为依赖传递给路由函数。而无需授权的路由,可以直接定义。

复制代码
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# 允许匿名访问的路由
@app.get("/public")
def read_public_data():
    return {"message": "This is a public endpoint"}

# 需要授权的路由
@app.get("/protected")
def read_protected_data(current_user: dict =Depends(get_current_user)):
    return {"message": f"Hello, {current_user['name']}"}

# 模拟登录生成 JWT 令牌的路由
@app.post("/token")
def login():
    # 这里省略了身份验证过程,只是直接生成一个 JWT 令牌
    token = jwt.encode({"id": 1, "name": "John Doe"}, JWT_SECRET_KEY, algorithm=ALGORITHM)
    return {"access_token": token, "token_type": "bearer"}
  • 公共路由 (/public):可以直接访问,不需要任何身份验证。
  • 受保护路由 (/protected):必须提供有效的 JWT 令牌才能访问。
  • 登录路由 (/token):生成一个 JWT 令牌,可以用于受保护的路

Depends(get_current_user) :在需要保护的路由中,通过依赖项注入 get_current_user,确保只有通过身份验证的用户才能访问。

FastAPI 中,建议通过依赖项注入(Depends)来获取当前用户的信息,而不是直接访问 request.user。这种方式更加灵活并且与 FastAPI 的设计哲学更一致。

在具体项目中,我们为了方便,往往通过中间件的方式进行定义和处理授权的过程。

通过authentiate函数处理验证用户令牌的有效性。

我们一般再main.py入口中加入中间件的处理即可。

复制代码
    # JWT auth, required
    app.add_middleware(
        AuthenticationMiddleware,
        backend=JwtAuthMiddleware(),
        on_error=JwtAuthMiddleware.auth_exception_handler,
    )

4、一些错误处理

当你在解码 JWT 时遇到 "Invalid audience" 错误,通常意味着在生成或解码 JWT 时,aud (audience) 声明没有正确设置或验证。以下是解决这个问题的步骤和说明:

1) 理解 aud (Audience) 声明

  • aud 是 JWT 中的一个可选声明,通常用于指定 JWT 的接收者(受众)。在解码 JWT 时,jwt.decode 会检查这个声明是否与预期的值匹配。

设置和检查 aud 声明, 生成 Token 时设置 aud

如果你希望 JWT 包含 aud 声明,可以在生成 token 时传递它。例如:

复制代码
to_encode = {
    "sub": "user_id",
    "aud": "your_audience",  # 设置aud声明
    "exp": datetime.utcnow() + timedelta(minutes=30)
}

token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, algorithm=settings.TOKEN_ALGORITHM)

解码 Token 时验证 aud

在解码 JWT 时,指定 audience 参数以匹配生成时的 aud

复制代码
try:
    payload = jwt.decode(
        token,
        settings.TOKEN_SECRET_KEY,
        algorithms=[settings.TOKEN_ALGORITHM],
        audience="your_audience"  # 验证aud声明
    )
    print(f"Decoded payload: {payload}")
except JWTError as e:
    print(f"Token decode failed: {e}")

2)Token decode failed: Subject must be a string.

"Token decode failed: Subject must be a string" 错误通常是由于 sub (subject) 声明的值不是字符串引起的。sub 是 JWT 中常用的一个声明,用来标识 token 的主体,比如用户 ID 或用户名。JWT 标准要求 sub 的值必须是字符串。

检查 sub 声明的值

首先,确保在生成 token 时,sub 声明的值是一个字符串:

复制代码
to_encode = {
    "sub": str(user_id),  # 确保 user_id 是字符串
    "aud": "your_audience",  # 其他字段
    "exp": datetime.utcnow() + timedelta(minutes=30)
}

token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, algorithm=settings.TOKEN_ALGORITHM)

如果 sub 的值是一个非字符串类型(如整数或其他对象),请将其转换为字符串:

复制代码
user_id = 123  # 假设 user_id 是一个整数
to_encode = {
    "sub": str(user_id),  # 将 user_id 转换为字符串
    "aud": "your_audience",
    "exp": datetime.utcnow() + timedelta(minutes=30)
}

token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, algorithm=settings.TOKEN_ALGORITHM)

3)schema如何移除敏感字段

在处理数据模型和模式(schema)时,特别是当涉及到敏感信息(如密码、身份信息等)时,有时需要从输出或序列化结果中移除这些敏感字段。根据你使用的库或框架,处理敏感字段的方法会有所不同。以下是一些常见的方法来移除敏感字段:

使用 Pydantic 的 exclude 参数

如果你在使用 Pydantic(例如在 FastAPI 中),你可以使用模型的 dict 方法的 exclude 参数来排除敏感字段。

复制代码
from pydantic import BaseModel

class User(BaseModel):
    username: str
    email: str
    password: str  # 敏感字段

user = User(username="user1", email="user1@example.com", password="secret")

# 创建一个不包含敏感字段的字典
user_dict = user.dict(exclude={"password"})

print(user_dict)

使用 SQLAlchemy 的 __mapper_args__

如果你使用 SQLAlchemy 并且希望从序列化结果中排除敏感字段,可以使用模型的 __mapper_args__ 进行配置。例如:

复制代码
from sqlalchemy import Column, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(String, primary_key=True)
    username = Column(String)
    email = Column(String)
    password = Column(String)  # 敏感字段

    def to_dict(self):
        # 移除敏感字段
        return {c.name: getattr(self, c.name) for c in self.__table__.columns if c.name != 'password'}

user = User(id="1", username="user1", email="user1@example.com", password="secret")
print(user.to_dict())

自定义序列化方法

如果你使用自定义模型或类,并且没有使用特定的库,你可以实现自定义序列化方法来排除敏感字段。

复制代码
class User:
    def __init__(self, username, email, password):
        self.username = username
        self.email = email
        self.password = password  # 敏感字段

    def to_dict(self):
        # 移除敏感字段
        return {
            "username": self.username,
            "email": self.email
        }

user = User(username="user1", email="user1@example.com", password="secret")
print(user.to_dict())
相关推荐
伍华聪15 天前
一问一答学习PyQT6,对比WxPython和PyQt6的差异
python开发
伍华聪17 天前
WxPython跨平台开发框架之使用PyInstaller 进行打包处理
python开发
伍华聪21 天前
WxPython跨平台开发框架之模块字段权限的管理
python开发
伍华聪22 天前
WxPython跨平台开发框架之动态菜单的管理和功能权限的控制
python开发
伍华聪24 天前
WxPython跨平台开发框架之前后端结合实现附件信息的上传及管理
python开发
伍华聪24 天前
WxPython跨平台开发框架之图标选择界面
python开发
伍华聪1 个月前
WxPython跨平台开发框架之列表数据的通用打印处理
python开发
伍华聪1 个月前
WxPython跨平台开发框架之复杂界面内容的分拆和重组处理
python开发
伍华聪1 个月前
WxPython跨平台开发框架之参数配置管理界面的设计和实现
python开发
伍华聪1 个月前
WxPython跨平台开发框架之表格数据导出到Excel并打开
python开发