PyJWT

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

1、
PyJWT

python-jose的差异

PyJWT

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

特点:

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

主要用法示例:

importjwtimportdatetime#创建一个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 importjwtfrom jose.exceptions importJWTError#创建一个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)exceptJWTError 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 importjwt, JWTErrorfrom jose.exceptions importExpiredSignatureError#定义密钥和有效负载
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)exceptExpiredSignatureError:print("Token has expired")exceptJWTError:print("Token is invalid")

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

如果我们需要再JWT的playload里面承载更多的信息,可以再claim中声明键值即可,你可以使用
python-jose

FastAPI
来实现 JWT 令牌生成操作。如下所示代码。

使用
FastAPI

python-jose
生成 JWT 令牌:

from fastapi importFastAPI, Depends, HTTPException, status, Requestfrom fastapi.responses importJSONResponsefrom jose importJWTError, jwtfrom datetime importdatetime, timedelta

app
=FastAPI()#配置项,通常从配置文件中加载 JWT_SECRET_KEY = 'your_jwt_secret_key'JWT_ISSUER= 'your_issuer'JWT_AUDIENCE= 'your_audience'JWT_EXPIRED_DAYS= 7ALGORITHM= 'HS256' defgenerate_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
defget_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__":importuvicorn
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 importDepends, HTTPException, statusfrom jose importJWTError, jwtfrom typing importOptional

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 isNone:raiseHTTPException(
status_code
=status.HTTP_401_UNAUTHORIZED,
detail
="Could not validate credentials",
headers
={"WWW-Authenticate": "Bearer"},
)
returnpayloadexceptJWTError:raiseHTTPException(
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 importFastAPI, Depends, HTTPException, statusfrom fastapi.security importOAuth2PasswordBearer

app
=FastAPI()

oauth2_scheme
= OAuth2PasswordBearer(tokenUrl="token")#允许匿名访问的路由 @app.get("/public")defread_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")deflogin():#这里省略了身份验证过程,只是直接生成一个 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}")exceptJWTError 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 importBaseModelclassUser(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 importColumn, Stringfrom sqlalchemy.ext.declarative importdeclarative_base

Base
=declarative_base()classUser(Base):__tablename__ = 'users'id= Column(String, primary_key=True)
username
=Column(String)
email
=Column(String)
password
= Column(String) #敏感字段 defto_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())

自定义序列化方法

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

classUser:def __init__(self, username, email, password):
self.username
=username
self.email
=email
self.password
= password #敏感字段 defto_dict(self):#移除敏感字段 return{"username": self.username,"email": self.email
}

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

标签: none

添加新评论