a3216 commited on
Commit
2e79d4d
·
verified ·
1 Parent(s): fb0cf86

sync from GitHub dad1933: fix(认证+恢复): 手表端长效token+refresh不吊销旧token+恢复容错

Browse files

Auto-synced from GitHub commit dad193322c9efba04d7d142428d87acc80931a08

Files changed (1) hide show
  1. app/api/user_files.py +34 -14
app/api/user_files.py CHANGED
@@ -66,6 +66,19 @@ def _token_ttl_sec() -> int:
66
  return get_settings().xtc_user_token_ttl_sec
67
 
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def _user_token_jti_from_raw(raw_token: str) -> str:
70
  """从原始 JWT 中无验签地提取 jti(用于吊销)。
71
 
@@ -118,11 +131,12 @@ def _resolve_alias(row_or_meta) -> str:
118
 
119
  # ===== JWT =====
120
 
121
- def _issue_user_token(user_id: int, username: str) -> str:
122
  """签发用户 JWT(NM2 / NL2)。
123
 
124
  - 加入 jti(唯一 ID)并落 user_tokens 表,支持服务端吊销
125
  - TTL 从 settings.xtc_user_token_ttl_sec 读取(默认 1 天,原 7 天)
 
126
  - 若配置了 XTC_JWT_KID 则写入 header 便于密钥轮换识别
127
  """
128
  settings = get_settings()
@@ -133,7 +147,7 @@ def _issue_user_token(user_id: int, username: str) -> str:
133
  code="server_misconfigured",
134
  )
135
  now = int(time.time())
136
- ttl = _token_ttl_sec()
137
  jti = uuid.uuid4().hex
138
  payload = {
139
  "type": "user_file",
@@ -143,6 +157,8 @@ def _issue_user_token(user_id: int, username: str) -> str:
143
  "exp": now + ttl,
144
  "jti": jti,
145
  }
 
 
146
  headers: dict = {}
147
  if settings.xtc_jwt_kid:
148
  headers["kid"] = settings.xtc_jwt_kid
@@ -484,7 +500,7 @@ async def register(request: Request):
484
  client_ip = rate_limit_store.get_client_ip(request)
485
  rate_limit_store.clear_auth_failures(client_ip=client_ip)
486
 
487
- token = _issue_user_token(user_id, username)
488
  return ok_with_cors({
489
  "token": token,
490
  "user": {"id": user_id, "username": username},
@@ -518,7 +534,7 @@ async def login(request: Request):
518
  client_ip = rate_limit_store.get_client_ip(request)
519
  rate_limit_store.clear_auth_failures(client_ip=client_ip)
520
 
521
- token = _issue_user_token(int(user["id"]), user["username"])
522
  return ok_with_cors({
523
  "token": token,
524
  "user": {"id": int(user["id"]), "username": user["username"]},
@@ -620,14 +636,18 @@ def _verify_user_token_for_refresh(token: str) -> dict:
620
  return payload
621
 
622
 
623
- def _do_refresh_token(user: dict):
624
  user_id = int(user["sub"])
625
  username = user["username"]
626
- # NM2:token rotation —— 吊销旧 jti,签发新 token
627
- old_jti = user.get("jti") or _user_token_jti_from_raw(user.get("_raw_token", ""))
628
- if old_jti:
629
- _revoke_user_token_jti(old_jti)
630
- token = _issue_user_token(user_id, username)
 
 
 
 
631
  return ok_with_cors({
632
  "token": token,
633
  "user": {"id": user_id, "username": username},
@@ -635,13 +655,13 @@ def _do_refresh_token(user: dict):
635
 
636
 
637
  @router.post("/refresh")
638
- async def refresh_token(user: dict = Depends(require_user_for_refresh)):
639
- return _do_refresh_token(user)
640
 
641
 
642
  @router.get("/refresh")
643
- async def refresh_token_get(user: dict = Depends(require_user_for_refresh)):
644
- return _do_refresh_token(user)
645
 
646
 
647
  # ===== 列表 =====
 
66
  return get_settings().xtc_user_token_ttl_sec
67
 
68
 
69
+ # 手表端长效 token TTL:3 天。
70
+ # 手表端(UA 含 "xtc")备份耗时长、并发请求多,1 天 TTL + rotation 机制容易导致
71
+ # 备份中途 401(refresh 吊销旧 token,其他并发请求仍在用旧 token)。
72
+ # 3 天 TTL 大幅减少 refresh 频率,从根本上避免 rotation 死锁。
73
+ _XTC_LONG_TOKEN_TTL_SEC = 3 * 86400
74
+
75
+
76
+ def _is_xtc_client(request: Request) -> bool:
77
+ """检测请求是否来自小天才手表端(UA 含 "xtc")。"""
78
+ ua = (request.headers.get("user-agent") or "").lower()
79
+ return "xtc" in ua
80
+
81
+
82
  def _user_token_jti_from_raw(raw_token: str) -> str:
83
  """从原始 JWT 中无验签地提取 jti(用于吊销)。
84
 
 
131
 
132
  # ===== JWT =====
133
 
134
+ def _issue_user_token(user_id: int, username: str, long_ttl: bool = False) -> str:
135
  """签发用户 JWT(NM2 / NL2)。
136
 
137
  - 加入 jti(唯一 ID)并落 user_tokens 表,支持服务端吊销
138
  - TTL 从 settings.xtc_user_token_ttl_sec 读取(默认 1 天,原 7 天)
139
+ - long_ttl=True 时签发 3 天长效 token(手表端专用,减少 refresh 频率)
140
  - 若配置了 XTC_JWT_KID 则写入 header 便于密钥轮换识别
141
  """
142
  settings = get_settings()
 
147
  code="server_misconfigured",
148
  )
149
  now = int(time.time())
150
+ ttl = _XTC_LONG_TOKEN_TTL_SEC if long_ttl else _token_ttl_sec()
151
  jti = uuid.uuid4().hex
152
  payload = {
153
  "type": "user_file",
 
157
  "exp": now + ttl,
158
  "jti": jti,
159
  }
160
+ if long_ttl:
161
+ payload["long_ttl"] = True
162
  headers: dict = {}
163
  if settings.xtc_jwt_kid:
164
  headers["kid"] = settings.xtc_jwt_kid
 
500
  client_ip = rate_limit_store.get_client_ip(request)
501
  rate_limit_store.clear_auth_failures(client_ip=client_ip)
502
 
503
+ token = _issue_user_token(user_id, username, long_ttl=_is_xtc_client(request))
504
  return ok_with_cors({
505
  "token": token,
506
  "user": {"id": user_id, "username": username},
 
534
  client_ip = rate_limit_store.get_client_ip(request)
535
  rate_limit_store.clear_auth_failures(client_ip=client_ip)
536
 
537
+ token = _issue_user_token(int(user["id"]), user["username"], long_ttl=_is_xtc_client(request))
538
  return ok_with_cors({
539
  "token": token,
540
  "user": {"id": int(user["id"]), "username": user["username"]},
 
636
  return payload
637
 
638
 
639
+ def _do_refresh_token(user: dict, request: Request):
640
  user_id = int(user["sub"])
641
  username = user["username"]
642
+ # NL7:不再吊销旧 token(rotation 改为共存)。
643
+ # 旧实现 refresh 时吊销旧 jti,但备份中其他并发请求可能仍持有旧 token,
644
+ # 5 分钟宽限期边界场景会导致 401 死锁。改为新旧 token 共存,旧 token 自然过期。
645
+ # 代价是多占几行 user_tokens 记录,由 _cleanup_expired_user_tokens 清理。
646
+ long_ttl = _is_xtc_client(request)
647
+ # 若旧 token 是长效的,新 token 也保持长效
648
+ if not long_ttl and user.get("long_ttl"):
649
+ long_ttl = True
650
+ token = _issue_user_token(user_id, username, long_ttl=long_ttl)
651
  return ok_with_cors({
652
  "token": token,
653
  "user": {"id": user_id, "username": username},
 
655
 
656
 
657
  @router.post("/refresh")
658
+ async def refresh_token(request: Request, user: dict = Depends(require_user_for_refresh)):
659
+ return _do_refresh_token(user, request)
660
 
661
 
662
  @router.get("/refresh")
663
+ async def refresh_token_get(request: Request, user: dict = Depends(require_user_for_refresh)):
664
+ return _do_refresh_token(user, request)
665
 
666
 
667
  # ===== 列表 =====