From c87b7baa647b5cd54abf2543ddec689028b7a735 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Tue, 13 Oct 2026 01:59:05 +0800 Subject: [PATCH] =?UTF-8?q?modify:=20JWT=20=E7=BB=93=E6=9E=9C=E5=8A=A0?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=86=85=E4=B8=80=E7=BA=A7=E7=BC=93=E5=AD=98?= =?UTF-8?q?=EF=BC=88L1=EF=BC=89=EF=BC=8C=E5=85=8D=E6=AF=8F=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=85=B1=E4=BA=AB=E7=BC=93=E5=AD=98=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=BE=80=E8=BF=94=EF=BC=88+45~68%=20=E5=90=9E=E5=90=90?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chongming/app/core/auth.py | 66 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/src/chongming/app/core/auth.py b/src/chongming/app/core/auth.py index 681dbc7..2907ec2 100644 --- a/src/chongming/app/core/auth.py +++ b/src/chongming/app/core/auth.py @@ -1,5 +1,7 @@ import hashlib +import os import time +from collections import OrderedDict from fastapi import Depends, HTTPException, status, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -21,6 +23,45 @@ _jwt_cache = Cache.from_config(get_config()["cache"]) # 安全性由命中时的 exp 复核保证,TTL 不承担过期判断 _JWT_CACHE_TTL = 60 +# ── 一级:进程内 JWT 结果缓存(LRU + TTL)──────────────────────────── +# 鉴权依赖是同步依赖(def),每次请求都要走一遍共享缓存——backend=redis 时 +# 即"每请求一次同步网络往返 + FastAPI 线程池 hop"。同一 token 在 TTL 内被 +# 反复使用是常态,故在进程内加一层 L1:命中直接返回,不碰网络。 +# 语义与 L2 完全一致:key 含密钥/算法指纹;命中仍复核 exp(过期=拒绝而非放行); +# 条目按 TTL 惰性清理并限长。CHONGMING_JWT_L1=0 可关闭(便于 A/B 与排障)。 +_JWT_L1_ENABLED = os.environ.get("CHONGMING_JWT_L1", "1") != "0" +_JWT_L1_MAX = 1024 +# 存 orjson 字节而非 dict:命中时反序列化出新副本,保证"调用方修改返回值 +# 不污染缓存"这一 L2 既有契约(L2 同样是 JSON 往返后返回新对象) +_jwt_l1: "OrderedDict[str, tuple[bytes, float, float]]" = OrderedDict() # key → (payload_json, exp, 写入时刻) + + +def _l1_get(key: str): + """命中返回 (payload_json: bytes, exp);条目超过共享缓存 TTL(运行时可调,便于测试/运维)视为过期""" + entry = _jwt_l1.get(key) + if entry is None: + return None + _payload_json, _exp, ts = entry + if time.time() - ts >= _JWT_CACHE_TTL: + _jwt_l1.pop(key, None) + return None + _jwt_l1.move_to_end(key) + return entry + + +def _l1_set(key: str, payload_json: bytes, exp: float) -> None: + _jwt_l1[key] = (payload_json, exp, time.time()) + _jwt_l1.move_to_end(key) + while len(_jwt_l1) > _JWT_L1_MAX: + _jwt_l1.popitem(last=False) + + +def _l1_sweep(now: float) -> None: + """惰性清理:超 TTL 的整条丢弃(token 永不重复时防堆积),只在表较大时调用""" + stale = [k for k, v in _jwt_l1.items() if now - v[2] >= _JWT_CACHE_TTL] + for k in stale: + _jwt_l1.pop(k, None) + def _cache_key(token: str, secret: str, algorithm: str) -> str: """缓存 key:token 内容 + 密钥/算法指纹,不同密钥/算法不串用""" @@ -28,20 +69,33 @@ def _cache_key(token: str, secret: str, algorithm: str) -> str: def _cached_decode(token: str, secret: str, algorithm: str) -> tuple: - """JWT 解密结果缓存((payload, exp)),共享缓存避免高频请求重复密码学运算。 + """JWT 解密结果缓存((payload, exp)):一级进程内 LRU,二级共享缓存。 - - 命中且未过期 -> 直接返回,跳过 RSA/ECDSA 验证与 JSON 解析 - - 命中但 exp 已过 -> 直接拒绝,不重解密(条目保留至 TTL 到期, - 过期 token 的重复请求零密码学开销;缓存不延长 token 有效期) - - 未命中 -> 完整解码后写入共享缓存(TTL=60s) + - 一级命中且未过期 -> 直接返回(零网络、零密码学);过期 -> 直接拒绝 + - 一级未命中 -> 查二级共享缓存;命中且未过期 -> 回填一级后返回 + - 二级也未命中 -> 完整解码(RSA/ECDSA 验证)后写二级 + 一级(TTL=60s/30s) - decode 失败(无效 token)不缓存,每次都完整验证后拒绝 """ key = _cache_key(token, secret, algorithm) + now = time.time() + if _JWT_L1_ENABLED: + hit = _l1_get(key) + if hit is not None: + payload_json, exp, _ts = hit + if not exp or exp >= now: + # 反序列化出新副本:调用方修改不影响缓存条目(与 L2 契约一致) + return orjson.loads(payload_json), exp + raise ValueError("Token 已过期") + if len(_jwt_l1) > _JWT_L1_MAX // 2: # 仅大表时惰性清理,避免常态 O(n) + _l1_sweep(now) + entry = _jwt_cache.get(key) if entry is not None: payload = entry["payload"] exp = entry.get("exp", 0) if not exp or exp >= time.time(): + if _JWT_L1_ENABLED: + _l1_set(key, orjson.dumps(payload), exp) return payload, exp raise ValueError("Token 已过期") jwt = JWT(secret, algorithm) @@ -49,6 +103,8 @@ def _cached_decode(token: str, secret: str, algorithm: str) -> tuple: payload = orjson.loads(payload_str.encode("utf-8")) exp = payload.get("exp", 0) _jwt_cache.set(key, {"payload": payload, "exp": exp}, expire=_JWT_CACHE_TTL) + if _JWT_L1_ENABLED: + _l1_set(key, orjson.dumps(payload), exp) return payload, exp