Python 后端 SDK qxj-backend-sdk
QXJ 后端 SDK 集合,供本后端(qxj-backend-admin)及其他后端服务使用,用于 验证 SM2 国密 JWT Token 与 国密加解密(含 64B 安全信封)。monorepo 结构,当前分支 fix/security-audit-20261004,三包版本均 0.1.0, requires-python ≥ 3.8,许可证 MIT。
Python 生态没有现成的 SM2 JWT 库,因此本仓库自研了完整链路:外层 SM2 签名 验签(公钥)+ 内层 SM3-HMAC 会话校验(Redis 会话密钥),底层基于纯 Python 的 gmssl>=3.2,跨平台无需编译原生扩展。
子包划分(源码统一在顶层 src/)
| 包 | 所在目录 | 职责 |
|---|---|---|
qxj-backend-sdk(顶层统一包) | 根目录 pyproject.toml | 高层统一接口 verify_access_token / sm4_gcm_encrypt / sm4_gcm_decrypt,自动组装 SessionKeyStore / TokenVerifier / Sm4GcmCrypto |
qxj-backend-sdk-auth | qxj-backend-sdk-auth/ | 仅认证:JWT 解析(SM2 外层验签 + SM3-HMAC 内层校验)、SessionKeyStore、CLI |
qxj-backend-sdk-cryptography | qxj-backend-sdk-cryptography/ | 仅加解密:SM2/SM3/SM3-HMAC/SM4-CBC/SM4-GCM + 64B 安全信封 |
依赖:cryptography 子包 → gmssl>=3.2(SM2/SM3/SM4 国密算法); auth 子包 → redis>=4.5(仅内层校验需要 Redis 客户端)。
目录结构:
qxj-backend-sdk/
├── pyproject.toml # 统一安装
├── src/
│ ├── qxj_backend_sdk/ # 高层统一接口(最常用)
│ │ ├── __init__.py # 导出 verify_access_token / sm4_gcm_encrypt / sm4_gcm_decrypt
│ │ └── highlevel.py # 实现:自动构造 SessionKeyStore + TokenVerifier + Sm4GcmCrypto
│ ├── qxj_backend_sdk_auth/ # 认证包(SM2 验签 + SM3-HMAC)
│ └── qxj_backend_sdk_cryptography/ # 加解密包(SM2/SM3/SM4 + 64B 信封)
├── qxj-backend-sdk-auth/pyproject.toml # 单独安装 auth
└── qxj-backend-sdk-cryptography/pyproject.toml # 单独安装 cryptography2
3
4
5
6
7
8
9
10
安装
统一安装(推荐)
pip install qxj-backend-sdk分别安装
pip install qxj-backend-sdk-auth # 仅认证
pip install qxj-backend-sdk-cryptography # 仅加解密2
开发安装(editable,支持热更新)
用可编辑安装,改完 src/ 下的源码即时生效,无需反复 reinstall:
# 安装两个包(auth + cryptography),源码改动立即生效
cd qxj-backend-sdk
pip install -e .
# 仅安装 auth
cd qxj-backend-sdk-auth
pip install -e .
# 仅安装 cryptography
cd qxj-backend-sdk-cryptography
pip install -e .2
3
4
5
6
7
8
9
10
11
热更新原理:
-e不会把代码复制到 site-packages,而是放一个.pth链接指向src/。Python import 时直接读源码,改完保存就生效。新增模块 文件有时需重启进程清缓存;改了pyproject.toml(依赖/入口点)需重新pip install -e .。
卸载
pip uninstall qxj-backend-sdk -y # 卸载统一安装的包
pip uninstall qxj-backend-sdk-auth -y # 卸载单独安装的 auth 子包
pip uninstall qxj-backend-sdk-cryptography -y # 卸载单独安装的 cryptography 子包2
3
调试用临时脚本统一放在项目根目录的 tmp/ 下(已加入 .gitignore,不会被 提交),用完直接 rm -rf tmp/ 清理。
SM2 JWT 格式与验签流程
签发方 qxj-backend-admin 使用 SM2withSM3 国密非对称签名签发 JWT: 私钥签名(服务器持有,SM4 加密存储在数据库),公钥验签(其他后端需公钥 验证 token)。
Token 结构
header_b64.payload_b64.signature_b64- header:
{"alg":"SM2","typ":"JWT"} - payload:标准 JWT claims(
exp、iat、jti、user_id、token_type、device_id、position等) - signature:SM2 签名值(r‖s,64 字节)的 base64url 编码
外层验签流程
1. 拆分 token → header_b64, payload_b64, sig_b64
2. signing_input = header_b64 + "." + payload_b64
3. hash = SM3(signing_input.encode('ascii')) → hex 字符串
4. signature = base64url_decode(sig_b64) → bytes → hex
5. public_key = "04" + server_public_key_hex (后端公钥不带 04 前缀,gmssl 需要补)
6. SM2.verify(public_key, hash, signature) → True/False2
3
4
5
6
内层校验:从 custom.device_id 提取设备 ID,按 device_id 从 Redis 查询 会话密钥,重算 SM3-HMAC 与 Token 中 custom.hmac 比对。
公钥获取接口
GET http://<后端地址>:4607/api/v3/admin/keymgr/qrcode_string/
→ JSON: {"data": {"public_key": "<128 hex chars>", "server_id": "uuid", ...}}2
公钥格式:hex 128 字符(64 字节,x‖y),不带 04 前缀。
高层统一接口(推荐使用)
顶层包封装了 auth + cryptography 的三大常用流程为三个模块级函数,显式传入 redis_client 或 redis_url + key_prefix(自动构建 SessionKeyStore), 屏蔽内部类的构造细节。设备 ID 自动从 token / 信封中提取,调用方无需 重复传入。
1. 后台 Token 校验接口:verify_access_token
功能:调用底层密码库 gmssl 的 SM2 验签函数,对 Token 签名进行验签, 确认 Token 由合法系统生成且未被篡改。主要功能:
- 对输入的 Token 进行格式解析,提取签名值、
custom字段; - 调用 gmssl 的
sm2_verify接口,对 Token 进行 SM2 签名验证(外层); - 自动从
custom.device_id提取设备 ID,按 device_id 从 Redis 查询会话 密钥,重算 SM3-HMAC 与 Token 中custom.hmac比对(内层); - 验签成功返回设备 ID、有效期;失败返回对应错误码。
调用场景:后端接收到客户端提交的登录 Token 时调用,验证其合法性。 典型场景包括接口访问身份校验、会话合法性校验、Token 有效期校验等。只有 在验证成功(result_code = 0)的情况下,系统才允许用户继续访问后续业务 接口。
函数原型:
def verify_access_token(
access: str,
public_key: str,
redis_client=None,
*,
skip_hmac: bool = False,
redis_url: Optional[str] = None,
key_prefix: str = "",
leeway: int = 0,
) -> tuple[str, str, int]2
3
4
5
6
7
8
9
10
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
access | str | JWT access token 字符串。 |
public_key | str | 服务器 SM2 公钥(128 hex,x‖y,不带 04 前缀)。可从 GET /api/v3/admin/keymgr/qrcode_string/ 获取。 |
redis_client | redis.Redis | None | 已连接的 redis 客户端,用于查会话密钥。与 redis_url 二选一;skip_hmac=True 时可不传。 |
skip_hmac | bool | 是否跳过内层 HMAC 校验,默认 False。True 仅做外层 SM2 验签 + 过期校验。 |
redis_url | str | None | redis 连接 URL,未传 redis_client 时使用。 |
key_prefix | str | redis key 前缀,需与后端 CACHES.KEY_PREFIX 对齐(如 "qxj:1:")。 |
leeway | int | 过期校验的时钟宽容(秒)。 |
返回值:三元组 (device_id, expire_time, result_code)
| 字段 | 类型 | 说明 |
|---|---|---|
device_id | str | 设备 ID(从 custom.device_id 提取,解析失败为空串)。 |
expire_time | str | 过期时间 Unix 时间戳字符串(解析失败为空串)。 |
result_code | int | 结果码,0 表示完全通过。 |
结果码:
| code | 说明 |
|---|---|
| 0 | 完全通过(外层 SM2 + 内层 HMAC 均通过) |
| 1 | 外层通过,内层跳过(device_id 为空或 skip_hmac=True) |
| -1 | Token 为空 |
| -2 | Token 格式解析失败 |
| -3 | 外层 SM2 验签失败 |
| -4 | Token 已过期 |
| -5 | custom 字段缺失 |
| -6 | 内层 HMAC 校验失败 |
| -7 | 未找到会话密钥(device_id 非空但 Redis 无对应 session_key) |
| -8 | Redis 查询异常 |
| -99 | 未知错误 |
示例:
import redis
from qxj_backend_sdk import verify_access_token
r = redis.Redis.from_url("redis://:pwd@host:6379/0")
pubkey = "..." # 服务器 SM2 公钥 128hex
device_id, exp, code = verify_access_token(access_token, pubkey, r, key_prefix="qxj:1:")
if code == 0:
print(f"通过, device_id={device_id}, exp={exp}")
elif code == 1:
print("外层通过,内层跳过")
else:
print(f"失败 code={code}")
# 仅外层(不查 Redis)
device_id, exp, code = verify_access_token(access_token, pubkey, skip_hmac=True)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意事项:
- 该函数调用底层 Python 库
gmssl(非 C 动态库libnspsmapi.so)实现 SM2/SM3,跨平台无需编译原生扩展; - Redis key 模板为
sm2:device_session_key:{device_id},前缀由key_prefix拼接; - 设备 ID 自动从 Token 中提取,调用方无需传入。
2. 后台明文数据封装接口:sm4_gcm_encrypt
调用 qxj_backend_sdk_cryptography.Sm4GcmCrypto 执行 SM4-GCM 模式加密:
- 根据
dev_id从 Redis 查询对应的会话密钥(SM2 握手协商产物,16B 或 32B;32B 时 SM4-128 实际取前 16B); - 生成 16B 随机 IV,对明文进行 SM4-GCM 加密并计算 16B GCM tag;
- 拼装 64B 标准通信头(version + main_cmd + sub_cmd + total_len + frame_seq + sender_id(36B) + enc_auth + IV(16B) + reserved)+ 密文 + 16B tag;
- 返回完整信封的 hex 字符串与结果码。
函数原型:
def sm4_gcm_encrypt(
dev_id: str,
plain: str,
redis_client=None,
*,
is_hex: bool = False,
redis_url: Optional[str] = None,
key_prefix: str = "",
) -> tuple[str, int]2
3
4
5
6
7
8
9
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
dev_id | str | 设备 ID(IDA),用于查会话密钥。 |
plain | str | 待加密的明文。is_hex=False 时为普通字符串,True 时为 hex 字符串。 |
redis_client | redis.Redis | None | 已连接的 redis 客户端。 |
is_hex | bool | 明文是否为 hex 格式,默认 False。 |
redis_url | str | None | redis 连接 URL,未传 redis_client 时使用。 |
key_prefix | str | redis key 前缀。 |
返回值:(ciphertext, result_code),ciphertext 为 64B 信封 hex (含帧头 + 密文 + 16B GCM tag),失败为空串。
| code | 说明 |
|---|---|
| 0 | 加密成功 |
| -1 | 明文为空 |
| -2 | 底层加密调用失败 |
| -3 | 未找到对应的会话密钥 |
| -7 | Redis 查询异常 |
| -8 | devID 格式不合法 |
| -9 | hex 字符串解析失败 |
| -99 | 未知错误 |
import redis
from qxj_backend_sdk import sm4_gcm_encrypt
r = redis.Redis.from_url("redis://:pwd@host:6379/0")
cipher_hex, code = sm4_gcm_encrypt(dev_id, "hello world", r, key_prefix="qxj:1:")
if code == 0:
print("密文:", cipher_hex)
# 加密 hex 明文
cipher_hex, code = sm4_gcm_encrypt(dev_id, "deadbeef", r, is_hex=True)2
3
4
5
6
7
8
9
10
11
同一份明文每次调用结果都不相同(因 IV 随机),属正常现象。会话密钥由 SM2 国密握手协商获得,存储于 Redis(key 模板
sm2:device_session_key:{device_id},TTL 7 天)。
3. 后台密文数据解封装接口:sm4_gcm_decrypt
调用 Sm4GcmCrypto 执行 SM4-GCM 解密与完整性校验:
- 对输入的 hex 密文解析,从 64B 帧头提取 sender ID(即设备 IDA)+ IV + enc_auth + 命令码;
- 按 sender ID 从 Redis 查会话密钥,调用 SM4-GCM 解密,重算 MAC 与帧尾 tag 比对;
- 返回解密后的明文与 MAC 校验结果;底层失败不抛异常,返回对应错误码。
函数原型:
def sm4_gcm_decrypt(
cipher: str,
redis_client=None,
*,
as_hex: bool = False,
dev_id: Optional[str] = None,
redis_url: Optional[str] = None,
key_prefix: str = "",
) -> tuple[str, int]2
3
4
5
6
7
8
9
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
cipher | str | 待解密的 64B 信封 hex 字符串。 |
redis_client | redis.Redis | None | 已连接的 redis 客户端。 |
as_hex | bool | True 返回 hex 字符串,False 返回 UTF-8 字符串,默认 False。 |
dev_id | str | None | 可选,覆盖信封中解析出的 sender(一般无需传)。 |
redis_url | str | None | redis 连接 URL,未传 redis_client 时使用。 |
key_prefix | str | redis key 前缀。 |
返回值:(plaintext, result_code)
| code | 说明 |
|---|---|
| 0 | 解密且 MAC 校验成功 |
| -1 | 密文为空 |
| -2 | 底层加密库调用失败 |
| -3 | 未找到对应的会话密钥(从信封 sender 解析失败) |
| -4 | 密文格式错误、非本算法产生或已被篡改 |
| -5 | 认证失败(GCM tag 不匹配) |
| -7 | Redis 查询异常 |
| -9 | hex 字符串解析失败 |
| -99 | 未知错误 |
import redis
from qxj_backend_sdk import sm4_gcm_decrypt
r = redis.Redis.from_url("redis://:pwd@host:6379/0")
plain, code = sm4_gcm_decrypt(cipher_hex, r, key_prefix="qxj:1:")
if code == 0:
print("明文:", plain)
# 返回 hex 明文
plain_hex, code = sm4_gcm_decrypt(cipher_hex, r, as_hex=True)2
3
4
5
6
7
8
9
10
11
设备 ID 自动从密文信封 sender 字段提取;不同 IV 对应的密文不同,但每次 解密都能正确还原同一明文。
认证子包 qxj-backend-sdk-auth
低层用法(高层接口之下):TokenVerifier + SessionKeyStore。
验证 Token(外层 + 内层)
from qxj_backend_sdk_auth import TokenVerifier, SessionKeyStore
verifier = TokenVerifier(
public_key_hex="服务器SM2公钥128hex",
session_store=SessionKeyStore(
redis_url="redis://:password@host:6379/0",
key_prefix="qxj:1:", # 对齐后端 CACHES.KEY_PREFIX
),
)
result = verifier.verify(token)
if result.ok:
# 外层 SM2 + 内层 HMAC 均通过
print("user_id:", result.user_id)
print("device_id:", result.device_id)
elif result.outer_ok:
# 外层通过,内层跳过(device_id 为空)
print("部分通过:", result.message)
else:
print("失败:", result.message)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
仅外层验证
from qxj_backend_sdk_auth import TokenVerifier
verifier = TokenVerifier(public_key_hex="服务器公钥")
result = verifier.verify(token)
if result.outer_ok:
print("外层 SM2 验签通过")2
3
4
5
6
解析 Token(不验签)
from qxj_backend_sdk_auth import (
get_header, get_payload, get_custom,
get_user_id, get_device_id, get_exp, get_token_type,
)
header = get_header(token)
payload = get_payload(token)
user_id = get_user_id(token)
device_id = get_device_id(token)2
3
4
5
6
7
8
9
命令行 CLI
# 完整验证
qxj-backend-sdk-auth verify -t <token> -k <pubkey> \
--redis-url 'redis://:pwd@host:6379/0' --key-prefix 'qxj:1:' -v
# 仅外层
qxj-backend-sdk-auth verify -t <token> -k <pubkey> --no-hmac -v
# 解析
qxj-backend-sdk-auth parse -t <token>
qxj-backend-sdk-auth parse -t <token> -f payload.custom.user_id2
3
4
5
6
7
8
9
10
加解密子包 qxj-backend-sdk-cryptography
基础算法
from qxj_backend_sdk_cryptography import (
sm2_generate_keypair, sm2_encrypt, sm2_decrypt,
sm2_sign, sm2_verify,
sm3, sm3_hmac,
sm4_cbc_encrypt, sm4_cbc_decrypt,
sm4_gcm_encrypt, sm4_gcm_decrypt,
)
# SM2
kp = sm2_generate_keypair()
cipher = sm2_encrypt(b"hello", kp.public_key)
plain = sm2_decrypt(cipher, kp.private_key)
sig = sm2_sign(b"msg", kp.private_key)
ok = sm2_verify(b"msg", sig, kp.public_key)
# SM3
h = sm3(b"data")
# SM3-HMAC
mac = sm3_hmac(b"key", b"msg")
# SM4-CBC(返回 iv+ciphertext)
key = b"0123456789abcdef"
iv_cipher = sm4_cbc_encrypt(b"hello", key)
plain = sm4_cbc_decrypt(iv_cipher, key)
# SM4-GCM(返回 ciphertext + 16B tag)
key = b"0123456789abcdef"
iv = b"0123456789ab" # 推荐 12 字节
cipher, tag = sm4_gcm_encrypt(b"hello", key, iv)
plain = sm4_gcm_decrypt(cipher, key, iv, tag)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
SM4-GCM 安全信封(Sm4GcmCrypto)
高层类封装完整流程:按 device_id 从 Redis 取会话密钥 → SM4-GCM 加密 → 拼 64B 信封协议头(自包含 IDA + IV + MAC)。
from qxj_backend_sdk_auth.redis import SessionKeyStore
from qxj_backend_sdk_cryptography import Sm4GcmCrypto, CryptoErrorCode, describe
store = SessionKeyStore(
redis_url="redis://:password@host:6379/0",
key_prefix="qxj:1:",
)
crypto = Sm4GcmCrypto(session_store=store)
# 加密字符串 -> (envelope_hex, code)
cipher_hex, code = crypto.encrypt_str(device_id, "helloworld")
if code == CryptoErrorCode.OK:
print("密文:", cipher_hex)
# 解密字符串 -> (plain, code)
plain, code = crypto.decrypt_str(cipher_hex)
if code == CryptoErrorCode.OK:
print("明文:", plain)
# 也支持 bytes 和 hex
cipher_hex, code = crypto.encrypt_bytes(device_id, b"\x00\x01\x02")
cipher_hex, code = crypto.encrypt_hex(device_id, "deadbeef")
plain_bytes, code = crypto.decrypt_bytes(cipher_hex)
plain_hex, code = crypto.decrypt_hex(cipher_hex)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
加密错误码:0 成功;-1 明文/密文为空;-2 底层加密调用失败;-3 未找到 会话密钥;-4 密文格式错误/被篡改;-5 认证失败(MAC 不匹配);-7 Redis 查询异常;-8 devID 格式不合法;-9 hex 解析失败。
三语言 SDK 调用对比
from qxj_backend_sdk import verify_access_token
device_id, expire_time, code = verify_access_token(
token, public_key, redis_client,
key_prefix="qxj"
)
# code: 0=通过 1=仅外层 -3=SM2失败 -6=HMAC失败2
3
4
5
6
7
import { sm4GcmEncrypt, sm4GcmDecrypt } from 'qxj-frontend-sdk'
// key 必须 32 字节(64 hex),iv 任意非空
const encrypted = sm4GcmEncrypt(key, iv, plaintext)
const decrypted = sm4GcmDecrypt(key, iv, encrypted)2
3
4
5
import { QxjSessionCrypto } from '@nsp/qxj-sdk'
// 帧头 64B + SM4-GCM 密文
const frame = QxjSessionCrypto.encryptFrame(sessionKey, ivHex, dataHex)
const plain = QxjSessionCrypto.decryptFrame(sessionKey, ivHex, frameHex)2
3
4
5
打包与发布
三个可独立发布的包,版本号集中在三个 _version.py 文件中:
src/qxj_backend_sdk/_version.py
src/qxj_backend_sdk_auth/_version.py
src/qxj_backend_sdk_cryptography/_version.py2
3
0. 前置准备
# setuptools < 61 不支持新 pyproject.toml 写法
python -m pip install --upgrade pip setuptools wheel build twine2
1. 本地构建 wheel 分发(最简单)
推荐只 build 根目录:根目录 pyproject.toml 的 [tool.setuptools.packages.find] where = ["src"] 没有 include/exclude 限制,自动发现 src/ 下三个 Python 包,全部打进同一个 wheel:
cd /path/to/qxj-backend-sdk
python -m build
# 产物:dist/qxj_backend_sdk-0.1.0-py3-none-any.whl + .tar.gz2
3
对方安装:
pip install qxj_backend_sdk-0.1.0-py3-none-any.whl
# 装完后三个 Python 包都可用:
# from qxj_backend_sdk import verify_access_token
# from qxj_backend_sdk_auth import TokenVerifier
# from qxj_backend_sdk_cryptography import Sm4GcmCrypto2
3
4
5
离线环境需先在联网机器上
pip download gmssl redis拉取依赖,连同 wheel 一起拷贝后pip install --no-index --find-links . xxx.whl。
1b.(可选)单独构建子包 wheel
子包 pyproject.toml 用 where = ["../src"] 指向父目录源码,必须从子包 目录内执行:
cd qxj-backend-sdk-auth
python -m build # 生成 dist/qxj_backend_sdk_auth-*.whl
cd ..
cd qxj-backend-sdk-cryptography
python -m build # 生成 dist/qxj_backend_sdk_cryptography-*.whl
cd ..2
3
4
5
6
7
子包 build 失败(典型报错
Backend subprocess exited when trying to invoke get_requires_for_build_wheel)优先用python -m build -v看完整 traceback;常见原因是 setuptools/wheel 过旧,或 WSL 跨/mnt/c访问 Windows 路径时../src相对路径解析问题。可改为在 Windows PowerShell 下 构建,或把源码拷贝到 WSL 原生路径后再 build。
2. 上传内部 PyPI(团队长期使用)
python -m twine upload --repository-url https://your-pypi.example.com/ dist/*对方:pip install --index-url https://your-pypi.example.com/simple/ qxj-backend-sdk
3. 直接从 Git 仓库安装
# 整包
pip install git+https://your-gitlab.example.com/qxj/qxj-backend-sdk.git
# 指定 tag/分支
pip install "git+https://your-gitlab.example.com/qxj/qxj-backend-sdk.git@v0.1.0"2
3
4
5
版本与发布流程
- 同步更新三个
_version.py的__version__与__version_info__; - 提交 commit,打 git tag(如
git tag v0.1.0); - 在三个目录分别执行
python -m build; - 选择方式 1 / 2 分发 wheel 或上传 PyPI;
- 推送 tag:
git push origin v0.1.0。
清理构建产物
rm -rf dist build *.egg-info src/*.egg-info
rm -rf qxj-backend-sdk-auth/dist qxj-backend-sdk-auth/build qxj-backend-sdk-auth/src/*.egg-info
rm -rf qxj-backend-sdk-cryptography/dist qxj-backend-sdk-cryptography/build qxj-backend-sdk-cryptography/src/*.egg-info2
3
或一行 PowerShell:
Get-ChildItem -Path .,qxj-backend-sdk-auth,qxj-backend-sdk-cryptography -Recurse -Include dist,build,*.egg-info -Directory | Remove-Item -Recurse -Force当前实际分发方式:qxj-backend-admin 与 mock-demo 的
vendor/目录; 内部 PyPI(GitLab Registry/devpi/Nexus)为占位渠道,未配置固定发布仓。
相关文档
- 后端 SDK API 参考
- 前端对应接口见 JS 前端 SDK 的「与后端的 对应关系」表
- 顶层 README 约 677 行(安装、三套错误码全表、打包发布全流程);两个子包 README 为精简入门并指向顶层