Metadata-Version: 2.4
Name: py-jwt-helper
Version: 0.1.2
Summary: jwt工具类，支持HS256、RS256、SM3(SM3+HMAC)、SM2(SM3+SM2)、SM4(SM3+SM4)五种签名算法
Author-email: ferdinand <gongjpa@gmail.com>
License: MIT
Keywords: jwt,jwt-helper,rsa,sm3,sm4
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.12
Requires-Dist: cryptography>=46.0.5
Requires-Dist: gmssl>=3.2.2
Requires-Dist: pyjwt[crypto]>=2.8.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110.0; extra == 'fastapi'
Requires-Dist: httpx>=0.28.1; extra == 'fastapi'
Description-Content-Type: text/markdown

# JWT的签发与验证组件

## 1. 组件整体说明

`py-jwt-helper` 是一个轻量级的 JWT 工具库，封装了密钥生成、JWT 签发、验签、载荷提取以及 JWKS 构建与解析等完整流程。

核心特性：

- **统一 Base64 密钥格式** — 对称密钥与非对称公私钥对外均以 Base64 字符串传递，内部按算法自动解析
- **五种签名算法** — HS256、RS256、SM3（SM3+HMAC）、SM2（SM3+SM2）、SM4（SM3+SM4）
- **JWK/JWKS 标准兼容** — 支持 RFC 7517 格式的 JWKS 构建、解析与 kid 自动匹配
- **非侵入式国密扩展** — 通过 PyJWT 的 `register_algorithm()` 注入国密算法，不修改 PyJWT 源码
- **FastAPI 集成** — 提供多系统签发隔离的鉴权依赖注入

## 2. 组件依赖加载

### 安装

```bash
# 基础安装
pip install py-jwt-helper

# 如需 FastAPI 集成
pip install py-jwt-helper[fastapi]
```

### 运行时依赖

| 依赖 | 版本要求 | 用途 |
|------|---------|------|
| PyJWT[crypto] | >= 2.8.0 | JWT 编解码与算法框架 |
| cryptography | >= 46.0.5 | RSA 密钥生成与 PEM 解析 |
| gmssl | >= 3.2.2 | 国密 SM2/SM3/SM4 算法实现 |

### 国密算法注册

使用 SM2、SM3、SM4 算法前，需先注册国密扩展：

```python
from py_jwt_helper.jwt.extensions import register_sm_crypto_algorithms

register_sm_crypto_algorithms()
```

> 建议在应用启动时调用一次即可。

### 导入

```python
from py_jwt_helper import JwtHelper, JwtPayload, JwkHelper, JwkKeyEntry
```

## 3. 核心接口功能说明

### 3.1. 算法支持与加签逻辑

#### 3.1.1. 支持的算法说明

| 算法 | 类型 | 密钥长度 | 签名方式                                  | JWK kty |
|------|------|---------|---------------------------------------|---------|
| HS256 | 对称 | 32 字节 | HMAC-SHA256(key, msg)                 | oct |
| SM3 | 对称 | 32 字节 | HMAC-SM3(key, msg)，与 HS256 结构相同       | oct |
| SM4 | 对称 | 16 字节 | SM4-CBC 加密取最后分组作 MAC                  | oct |
| RS256 | 非对称 | 2048 位 | RSASSA-PKCS1-v1_5，SHA-256哈希 + RSA私钥签名 | RSA |
| SM2 | 非对称 | 私钥 32 字节 / 公钥 64 字节 | SM3哈希 + SM2椭圆曲线签名                     | EC (crv=SM2) |

- 对称算法：加签与验签使用同一密钥
- 非对称算法：私钥加签，公钥验签；SM2 由于内部 `prepare_key` 兼容，私钥也可用于验签

#### 3.1.2. 生成各算法的密钥的接口和结果说明

##### `JwtHelper.generate_key_or_keypair(alg)`

根据算法名生成密钥，统一返回 Base64 编码格式。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `alg` | `str` | 算法名称，支持 `HS256`、`SM3`、`SM4`、`RS256`、`SM2` |

**返回值：**

- 对称算法（HS256 / SM3 / SM4）：返回 `str` — Base64 编码的密钥字符串
- 非对称算法（RS256 / SM2）：返回 `Dict[str, str]`，结构为：

```python
{
    "private_key": "Base64编码的私钥",
    "public_key":  "Base64编码的公钥"
}
```

**调用示例：**

```python
from py_jwt_helper import JwtHelper
from py_jwt_helper.jwt.extensions import register_sm_crypto_algorithms

register_sm_crypto_algorithms()

## 对称算法 — 返回单个 Base64 字符串
hs256_key = JwtHelper.generate_key_or_keypair("HS256")
## 例: "KU49TG81ii+VKtP2WV4/jaZGh4AOvyW5WUXcxnvsqMs="

sm3_key = JwtHelper.generate_key_or_keypair("SM3")
sm4_key = JwtHelper.generate_key_or_keypair("SM4")

# 非对称算法 — 返回 dict
rsa_keys = JwtHelper.generate_key_or_keypair("RS256")
# 例: {"private_key": "LS0tLS1CRUdJTi...", "public_key": "LS0tLS1CRUdJTi..."}

sm2_keys = JwtHelper.generate_key_or_keypair("SM2")
# 例: {"private_key": "IesuWFlC5RgjS7IX...", "public_key": "BHzn7neIZZX6t3VA..."}
```

##### `JwtHelper.get_public_key_from_private(secret_key, alg)`

基于非对称算法的私钥推导其对应的公钥。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `secret_key` | `str` | Base64 编码的私钥字符串 |
| `alg` | `str` | 算法类型，仅支持 `RS256`、`SM2` |

**返回值：** `str` — Base64 编码的公钥字符串

**调用示例：**

```python
sm2_keys = JwtHelper.generate_key_or_keypair("SM2")
pub_key = JwtHelper.get_public_key_from_private(sm2_keys["private_key"], "SM2")
assert pub_key == sm2_keys["public_key"]
```

#### 3.1.3. 基于各算法生成 JWT 的接口说明

##### `JwtHelper.create_jwt(secret_key, sub, expire_second, ...)`

创建一个完整的 JWT 字符串。

**参数：**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `secret_key` | `str` | 是 | Base64 编码的密钥。对称算法为共享密钥；非对称算法为私钥 |
| `sub` | `str` | 是 | JWT 主题内容或标识，通常存放业务数据 |
| `expire_second` | `int` | 是 | 有效期（秒）。负值可生成已过期的 token 用于测试 |
| `iss` | `str` | 否 | 签发者身份标识 |
| `before_second` | `int` | 否 | 延迟生效时间（秒），生成 `nbf` 声明。必须小于 `expire_second` |
| `alg` | `str` | 否 | 签名算法，默认 `"RS256"` |
| `kid` | `str` | 否 | 密钥 ID，写入 JWT Header，用于 JWKS 场景中按 kid 匹配验签密钥 |

**返回值：** `str` — 编码后的 JWT 字符串

**调用示例：**

```python
from py_jwt_helper import JwtHelper

# 对称算法签名
key = JwtHelper.generate_key_or_keypair("HS256")
token = JwtHelper.create_jwt(
    secret_key=key,
    sub="user_001",
    expire_second=3600,
    iss="my_service",
    alg="HS256",
    kid="hs256-key-1"
)

# 非对称算法签名（私钥签发）
rsa_keys = JwtHelper.generate_key_or_keypair("RS256")
token = JwtHelper.create_jwt(
    secret_key=rsa_keys["private_key"],
    sub="user_002",
    expire_second=1800,
    iss="auth_server",
    alg="RS256",
    kid="rsa-key-1"
)

# sub 中存放 JSON 业务数据
import json
payload_data = {"providerId": "hr", "id": "00001", "name": "张三"}
token = JwtHelper.create_jwt(
    secret_key=rsa_keys["private_key"],
    sub=json.dumps(payload_data, ensure_ascii=False),
    expire_second=86400 * 30,
    alg="RS256"
)
```

### 3.2. 验签相关逻辑

#### 3.2.1. 多密钥/公钥 + kid 生成 JWKS 信息说明

##### `JwkKeyEntry` — JWKS 构建入参实体

| 字段 | 类型 | 说明 |
|------|------|------|
| `alg` | `str` | 算法名称（HS256 / SM3 / SM4 / RS256 / SM2） |
| `kid` | `str` | 密钥 ID，用于在 JWKS 中唯一标识该密钥 |
| `key` | `str` | Base64 编码的验签密钥。对称算法为共享密钥，非对称算法为公钥 |

##### `JwkHelper.build_jwks(keys)`

根据传入的验签密钥列表构建标准的 JWKS（JSON Web Key Set）结构。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `keys` | `List[JwkKeyEntry]` | JwkKeyEntry 列表 |

**返回值：** `Dict` — 标准 JWKS 结构

```python
{
    "keys": [
        {
            "alg": "HS256",
            "kid": "hs256-1",
            "use": "sig",
            "kty": "oct",
            "k": "<Base64Url 编码的对称密钥>"
        },
        {
            "alg": "RS256",
            "kid": "rsa-1",
            "use": "sig",
            "kty": "RSA",
            "n": "<Base64Url 编码的模数>",
            "e": "<Base64Url 编码的指数>"
        },
        {
            "alg": "SM2",
            "kid": "sm2-1",
            "use": "sig",
            "kty": "EC",
            "crv": "SM2",
            "x": "<Base64Url 编码的 X 坐标>",
            "y": "<Base64Url 编码的 Y 坐标>"
        }
    ]
}
```

**调用示例：**

```python
from py_jwt_helper import JwtHelper, JwkHelper, JwkKeyEntry

# 生成各算法密钥
hs256_key = JwtHelper.generate_key_or_keypair("HS256")
sm3_key = JwtHelper.generate_key_or_keypair("SM3")
rsa_keys = JwtHelper.generate_key_or_keypair("RS256")
sm2_keys = JwtHelper.generate_key_or_keypair("SM2")

# 构建入参列表（非对称算法使用公钥）
entries = [
    JwkKeyEntry(alg="HS256", kid="hs256-1", key=hs256_key),
    JwkKeyEntry(alg="SM3", kid="sm3-1", key=sm3_key),
    JwkKeyEntry(alg="RS256", kid="rsa-1", key=rsa_keys["public_key"]),
    JwkKeyEntry(alg="SM2", kid="sm2-1", key=sm2_keys["public_key"]),
]

jwks = JwkHelper.build_jwks(entries)
# jwks 可直接作为 JSON 响应暴露到 /.well-known/jwks.json 端点
```

#### 3.2.2. 基于带有 kid 的 JWT 或 JWKS 中匹配验签密钥/公钥的说明

##### `JwkHelper.get_key_from_jwk(jwk)`

从单个 JWK 字典中提取验签密钥。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `jwk` | `Dict` | 单个 JWK 字典（非 JWKS，需从 `jwks["keys"][i]` 获取） |

**返回值：** `str` — Base64 编码的验签密钥字符串

##### `JwkHelper.resolve_key_from_jwks(jwks, token)`

根据 JWT Header 中的 `kid` 从 JWKS 中自动匹配对应的 JWK，并提取验签密钥。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `jwks` | `Dict` | 标准 JWKS 字典 `{"keys": [...]}` |
| `token` | `str` | JWT 字符串（Header 中需包含 `kid`） |

**返回值：** `str` — Base64 编码的验签密钥字符串

**调用示例：**

```python
from py_jwt_helper import JwtHelper, JwkHelper, JwkKeyEntry

# 1. 构建多密钥 JWKS
rsa_keys = JwtHelper.generate_key_or_keypair("RS256")
sm2_keys = JwtHelper.generate_key_or_keypair("SM2")

entries = [
    JwkKeyEntry(alg="RS256", kid="rsa-1", key=rsa_keys["public_key"]),
    JwkKeyEntry(alg="SM2", kid="sm2-1", key=sm2_keys["public_key"]),
]
jwks = JwkHelper.build_jwks(entries)

# 2. 用私钥签发带 kid 的 JWT
token = JwtHelper.create_jwt(
    secret_key=rsa_keys["private_key"],
    sub="user_001",
    expire_second=3600,
    alg="RS256",
    kid="rsa-1"
)

# 3. 根据 token 中的 kid 自动匹配并提取验签密钥
resolved_key = JwkHelper.resolve_key_from_jwks(jwks, token)
# resolved_key 将匹配 kid="rsa-1" 对应的公钥

# 4. 验签
payload = JwtHelper.verify_jwt(token, resolved_key)
```

#### 3.2.3. JWT 验签和 Payload 信息获取的结构说明

##### `JwtHelper.verify_jwt(token, secret_key)`

校验 JWT 的签名有效性、过期时间、生效时间等。校验失败会抛出 `jwt.exceptions.PyJWTError` 的相关子类。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `token` | `str` | JWT 字符串 |
| `secret_key` | `str` | Base64 编码的验签密钥。对称算法为共享密钥；非对称算法为公钥 |

**返回值：** `JwtPayload` — 载荷实体对象

**可能抛出的异常：**

| 异常类 | 说明 |
|--------|------|
| `jwt.exceptions.DecodeError` | token 格式错误或 Header 缺少算法 |
| `jwt.exceptions.ExpiredSignatureError` | token 已过期 |
| `jwt.exceptions.ImmatureSignatureError` | token 尚未生效（nbf） |
| `jwt.exceptions.InvalidSignatureError` | 签名不匹配 |

##### `JwtHelper.extract_payload(token)`

忽略签名，仅提取 JWT 的 Payload 部分（适用于不需要验签的调试场景）。

**参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `token` | `str` | JWT 字符串 |

**返回值：** `JwtPayload` — 载荷实体对象

##### `JwtPayload` — 载荷实体

继承自 `dict`，可直接作为字典使用，同时提供以下便捷属性：

| 属性 | 类型 | 说明 |
|------|------|------|
| `exp_time` | `datetime \| None` | 过期时间（UTC），对应 `exp` 声明 |
| `iat_time` | `datetime \| None` | 签发时间（UTC），对应 `iat` 声明 |
| `nbf_time` | `datetime \| None` | 生效时间（UTC），对应 `nbf` 声明 |
| `iss` | `str \| None` | 签发者，对应 `iss` 声明 |
| `sub` | `str \| None` | 主题内容，对应 `sub` 声明 |

所有标准 JWT 声明及自定义字段均可通过字典访问，如 `payload["sub"]`、`payload.get("custom_field")`。

**调用示例：**

```python
from py_jwt_helper import JwtHelper

# 验签并获取 payload
try:
    payload = JwtHelper.verify_jwt(token, public_key)
    print(payload.sub)           ## "user_001"
    print(payload.iss)           ## "auth_server"
    print(payload.exp_time)      ## datetime(2026, 5, 26, 12, 0, 0, tzinfo=UTC)
    print(payload.iat_time)      ## datetime(2026, 5, 25, 12, 0, 0, tzinfo=UTC)
    print(payload["sub"])        ## 字典方式访问，等价于 payload.sub
except jwt.exceptions.ExpiredSignatureError:
    print("Token 已过期")
except jwt.exceptions.InvalidSignatureError:
    print("签名无效")

# 仅提取 payload（不验签）
payload = JwtHelper.extract_payload(token)
print(payload.sub)
```

**完整端到端示例：**

```python
from py_jwt_helper import JwtHelper, JwkHelper, JwkKeyEntry
from py_jwt_helper.jwt.extensions import register_sm_crypto_algorithms

register_sm_crypto_algorithms()

# 1. 生成密钥
rsa_keys = JwtHelper.generate_key_or_keypair("RS256")

# 2. 构建入参并生成 JWKS
entries = [JwkKeyEntry(alg="RS256", kid="rsa-1", key=rsa_keys["public_key"])]
jwks = JwkHelper.build_jwks(entries)

# 3. 签发 JWT
token = JwtHelper.create_jwt(
    secret_key=rsa_keys["private_key"],
    sub="user_001",
    expire_second=3600,
    iss="auth_server",
    alg="RS256",
    kid="rsa-1"
)

# 4. 从 JWKS 中自动匹配密钥并验签
resolved_key = JwkHelper.resolve_key_from_jwks(jwks, token)
payload = JwtHelper.verify_jwt(token, resolved_key)
print(f"用户: {payload.sub}, 签发者: {payload.iss}, 过期: {payload.exp_time}")
```
