You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Authlib中JWTClaims类型异常导致JWT exp验证失败

Authlib JWT解码后声明类型异常导致exp验证失败

我研究Authlib包好几天了,想用authlib.jose的JsonWebToken类的encode()和decode()复现标准JWT认证流程。编码没问题,能正常生成JWT,但解码验证环节卡壳了——调用validate()时一直抛出异常:

invalid_claim: Invalid claim "exp"

排查后发现,claims_options里用的validate_exp()函数会在_validate_numeric_time()中检查"exp"声明的类型,要求是int或float,但JWTClaims实例解码后所有键值全是str类型,exp这类时间相关声明的float类型没保留,直接导致了这个异常。

可复现代码

from typing import Any, List, Literal, Optional, Self, Union
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from authlib.jose import JsonWebToken, JWTClaims
from authlib.jose.errors import InvalidClaimError

# 假设SECRETS是已定义的配置字典
SECRETS = {
    "PRIVATE_KEY_FILE_PATH": "private_key.pem",
    "PUBLIC_KEY_FILE_PATH": "public_key.pem"
}

jwt = JsonWebToken(["RS256"])

# 读取密钥文件
private_pem_file = open(SECRETS["PRIVATE_KEY_FILE_PATH"])
private_key = private_pem_file.read()
private_pem_file.close()

public_pem_file = open(SECRETS["PUBLIC_KEY_FILE_PATH"])
public_key = public_pem_file.read()
public_pem_file.close()

header = {"alg": "RS256"}

# 配置JWT有效期
lifespan: timedelta = timedelta(minutes=15)
current_time: datetime = datetime.now(tz=timezone(timedelta(hours=5, minutes=30)))
current_time_plus_lifespan: datetime = current_time + lifespan

# 生成时间声明
issued_at: float = current_time.timestamp()
expiration_time: float = current_time_plus_lifespan.timestamp()
not_before: float = current_time.timestamp()
jwt_id: uuid4 = uuid4()

# 构造payload(注意修正UUID序列化)
payload: dict[str, Union[float, List[str], str]] = {
    "iss": "HarshitDoshi",
    "sub": "Harshit.Doshi@example.org",
    "aud": ["https://example.org", "https://authentication.example.org"],
    "iat": issued_at,
    "exp": expiration_time,
    "nbf": not_before,
    "jti": str(jwt_id),
}

# 编码JWT
jwt_encoded = jwt.encode(header, payload, private_key, check=True).decode("utf-8")

# 解码JWT(修正变量名错误)
jwt_decoded = jwt.decode(
    s=jwt_encoded,
    key=public_key,
    claims_options={
        "iss": {
            "essential": True,
            "value": "HarshitDoshi",
        },
        "sub": {
            "essential": True,
            "value": "Harshit.Doshi@example.org",
        },
        "aud": {
            "essential": True,
            "values": ['https://example.org', 'https://authentication.example.org'],
        },
        "iat": {
            "essential": True,
            "validate": JWTClaims.validate_iat,
        },
        "exp": {
            "essential": True,
            "validate": JWTClaims.validate_exp,
        },
        "nbf": {
            "essential": True,
            "validate": JWTClaims.validate_nbf,
        },
        "jti": {
            "essential": True,
            "value": str(jwt_id),
        },
    },
)

# 验证声明
jwt_decoded.validate()

调试输出

我用print调试输出了JWTClaims的键值类型:

for k in jwt_decoded.keys():
    print(f"\n{k}: {jwt_decoded[k]} - {type(jwt_decoded[k])}")

结果显示所有值都是字符串类型(包括本应是数值的时间声明和数组类型的aud):

iss: HarshitDoshi - <class 'str'>
sub: Harshit.Doshi@example.org - <class 'str'>
aud: ['https://example.org', 'https://authentication.example.org'] - <class 'str'>
iat: 1681022938.106472 - <class 'str'>
exp: 1681023838.106472 - <class 'str'>
nbf: 1681022938.106472 - <class 'str'>

问题原因与解决方案

核心原因

  1. UUID对象序列化异常:原代码中payload的jti字段直接传入UUID实例,JSON标准序列化器无法处理该类型,导致Authlib编码时异常处理整个payload,将其强制转为字符串,最终解码后所有字段都变成字符串类型。
  2. 变量名笔误:解码时使用了未定义的encoded_jwt变量,正确变量名应为jwt_encoded。

修复步骤

  1. 修正UUID序列化:将payload中的jti字段改为字符串类型的UUID:"jti": str(jwt_id),确保JSON序列化正常。
  2. 修正解码变量名:把jwt.decode(s=encoded_jwt, ...)改为jwt.decode(s=jwt_encoded, ...)。
  3. (可选)手动类型转换:若解码后部分字段仍为字符串,可在验证前手动转换类型:
# 转换时间声明为数值类型
for claim in ['iat', 'exp', 'nbf']:
    jwt_decoded[claim] = float(jwt_decoded[claim])
# 转换aud为列表(如果被转为字符串)
import json
if isinstance(jwt_decoded['aud'], str):
    jwt_decoded['aud'] = json.loads(jwt_decoded['aud'])

内容的提问来源于stack exchange,提问作者hash

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 01:37:59