Files
chongming-core/test/test_jwt_cache.py
T
2026-08-23 05:10:27 +08:00

239 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
JWT 解析共享缓存测试(auth.py _cached_decode / jwt_auth_dependency)
验证:
1. 同 token 重复鉴权只解密一次(缓存命中),不同 token 独立缓存
2. TTL 过期后重新解码(条目自然失效)
3. 缓存条目 exp 已过期:命中即拒绝,不重解密(缓存不延长 token 有效期)
4. 无效 token 不缓存失败:每次都走完整验证(decode 调用计数递增)
5. 缓存 key 含密钥/算法指纹:同 token 不同密钥不串用
6. 返回值为独立对象:调用方修改不影响缓存
7. 端到端:真实 chongming_jwt 签发 -> 鉴权成功 -> 二次命中缓存
运行: uv run python test_jwt_cache.py
"""
import sys
import tempfile
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parent / "utils" / "worker" / "src"))
import orjson
from fastapi.security import HTTPAuthorizationCredentials
from chongming.app.core import auth as auth_mod
from chongming.app.core.auth import jwt_auth_dependency
from chongming_cache import Cache
from chongming_jwt import JWT
from chongming_config import get_config
failures = []
def check(name, cond, detail=""):
status = "PASS" if cond else "FAIL"
print(f"[{status}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
failures.append(name)
config = get_config()
SECRET = config["security"]["secret_key"]
ALGORITHM = config["security"]["algorithm"]
# 每个测试使用独立临时 diskcache,隔离真实缓存目录
_tmp = tempfile.TemporaryDirectory()
def fresh_cache():
auth_mod._jwt_cache = Cache(backend="diskcache", cache_store_path=_tmp.name)
auth_mod._jwt_cache.clear()
def make_creds(token):
return HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
# ---- 1. 缓存命中与隔离 ----
def test_cache_hit_and_isolation():
fresh_cache()
class CountingJWT:
calls = 0
def __init__(self, secret, algorithm):
pass
def decode(self, token):
CountingJWT.calls += 1
return orjson.dumps({"sub": token, "exp": int(time.time()) + 3600}).decode()
auth_mod.JWT = CountingJWT
t1, t2 = "token-1", "token-2"
p1 = jwt_auth_dependency(credentials=make_creds(t1))
p2 = jwt_auth_dependency(credentials=make_creds(t1))
p3 = jwt_auth_dependency(credentials=make_creds(t2))
check("同 token 二次鉴权命中缓存(只解密一次)", CountingJWT.calls == 2, f"calls={CountingJWT.calls}")
check("payload 解析正确", p1["sub"] == "token-1" and p2["sub"] == "token-1", str(p1))
check("不同 token 独立缓存", p3["sub"] == "token-2", str(p3))
check("缓存中已写入条目", len(auth_mod._jwt_cache) >= 2, str(len(auth_mod._jwt_cache)))
# ---- 2. TTL 过期后重新解码 ----
def test_ttl_expiry_redecodes():
fresh_cache()
auth_mod._JWT_CACHE_TTL = 1 # 缩短 TTL 加速测试
class TtlJWT:
calls = 0
def __init__(self, secret, algorithm):
pass
def decode(self, token):
TtlJWT.calls += 1
return orjson.dumps({"sub": "u1", "exp": int(time.time()) + 3600}).decode()
auth_mod.JWT = TtlJWT
creds = make_creds("ttl-token")
p1 = jwt_auth_dependency(credentials=creds)
p2 = jwt_auth_dependency(credentials=creds)
check("TTL 内命中", TtlJWT.calls == 1 and p2["sub"] == "u1", f"calls={TtlJWT.calls}")
time.sleep(1.2) # 等待 TTL 过期
p3 = jwt_auth_dependency(credentials=creds)
check("TTL 过期后重新解码", TtlJWT.calls == 2 and p3["sub"] == "u1", f"calls={TtlJWT.calls}")
# ---- 3. 缓存条目 exp 已过期:命中即拒绝,不重解密 ----
def test_cached_expired_rejected():
fresh_cache()
class NeverJWT:
calls = 0
def __init__(self, secret, algorithm):
pass
def decode(self, token):
NeverJWT.calls += 1
raise AssertionError("不应触发解码(命中缓存即应拒绝)")
auth_mod.JWT = NeverJWT
# 预置一个 exp 已过期的缓存条目(模拟缓存写入后 token 到期)
key = auth_mod._cache_key("expired-token", SECRET, ALGORITHM)
auth_mod._jwt_cache.set(
key, {"payload": {"sub": "u1"}, "exp": int(time.time()) - 10}, expire=60
)
for i in range(2):
try:
jwt_auth_dependency(credentials=make_creds("expired-token"))
check(f"缓存过期条目第 {i+1} 次应 401", False, "未抛异常")
except Exception as e:
check(f"缓存过期条目第 {i+1} 次 401", getattr(e, "status_code", None) == 401, str(e))
check("过期条目命中后未重解密(两次都直接拒绝)", NeverJWT.calls == 0, f"calls={NeverJWT.calls}")
check("过期条目保留在缓存(TTL 内持续拒绝)", key in auth_mod._jwt_cache)
# ---- 4. 无效 token 失败不缓存 ----
def test_invalid_token_not_cached():
fresh_cache()
class BadJWT:
calls = 0
def __init__(self, secret, algorithm):
pass
def decode(self, token):
BadJWT.calls += 1
raise ValueError("Invalid signature")
auth_mod.JWT = BadJWT
creds = make_creds("bad-token")
for _ in range(2):
try:
jwt_auth_dependency(credentials=creds)
check("无效 token 应 401", False)
except Exception as e:
ok = getattr(e, "status_code", None) == 401
check("无效 token 401", ok, str(e))
check("无效 token 每次都完整验证(失败不缓存)", BadJWT.calls == 2, f"calls={BadJWT.calls}")
check("缓存无失败条目", len(auth_mod._jwt_cache) == 0, str(len(auth_mod._jwt_cache)))
# ---- 5. 缓存 key 含密钥/算法指纹 ----
def test_key_scoped_to_secret():
fresh_cache()
class KeyJWT:
calls = 0
def __init__(self, secret, algorithm):
pass
def decode(self, token):
KeyJWT.calls += 1
return orjson.dumps({"sub": token, "exp": int(time.time()) + 3600}).decode()
auth_mod.JWT = KeyJWT
creds = make_creds("same-token")
jwt_auth_dependency(credentials=creds) # secret A
jwt_auth_dependency(credentials=creds) # secret A -> 命中
auth_mod._cached_decode("same-token", "secret-B", ALGORITHM) # secret B -> miss
check("同 token 不同密钥独立缓存", KeyJWT.calls == 2, f"calls={KeyJWT.calls}")
# ---- 6. 返回值独立对象:修改不影响缓存 ----
def test_payload_object_independent():
fresh_cache()
class FixedJWT:
def __init__(self, secret, algorithm):
pass
def decode(self, token):
return orjson.dumps({"sub": "u1", "role": "admin", "exp": int(time.time()) + 3600}).decode()
auth_mod.JWT = FixedJWT
p1 = jwt_auth_dependency(credentials=make_creds("t-copy"))
p1["role"] = "hacker"
p2 = jwt_auth_dependency(credentials=make_creds("t-copy"))
check("修改返回值不污染缓存", p2["role"] == "admin", str(p2))
# ---- 7. 端到端:真实 JWT 签发 -> 鉴权 -> 命中缓存 ----
def test_e2e_real_jwt():
fresh_cache()
auth_mod.JWT = JWT # 恢复真实 JWT 类
jwt = JWT(SECRET, ALGORITHM)
token = jwt.encode(orjson.dumps({"sub": "1", "username": "alice"}).decode(), expires_in_seconds=3600)
creds = make_creds(token)
p1 = jwt_auth_dependency(credentials=creds)
p2 = jwt_auth_dependency(credentials=creds)
check("真实 JWT 鉴权成功", p1["sub"] == "1" and p1["username"] == "alice", str(p1))
check("二次调用命中缓存", auth_mod._cache_key(token, SECRET, ALGORITHM) in auth_mod._jwt_cache)
try:
jwt_auth_dependency(credentials=None)
check("缺 header 应 401", False)
except Exception as e:
check("缺 header 401", getattr(e, "status_code", None) == 401)
test_cache_hit_and_isolation()
test_ttl_expiry_redecodes()
test_cached_expired_rejected()
test_invalid_token_not_cached()
test_key_scoped_to_secret()
test_payload_object_independent()
test_e2e_real_jwt()
_tmp.cleanup()
print()
if failures:
print(f"共 {len(failures)} 项失败: {failures}")
sys.exit(1)
print("全部通过")