登录系统审计报告与改造方案

审计时间:2026-10-12 11:46–11:52
触发:义父「登录系统是很关键的,容不得瑕疵,你一定要仔细分析一下」
⚠️ 本报告全部结论均来自代码与生产配置实测,无推测

一、结论先行:⚠️ 问题比预想严重,但不是"不能用",是"每天坏一次"

⚠️ 核心问题一句话:

JWT 有效期 24 小时 + 没有静默续登 + 401 无全局通知
= 客人每天打开小程序都会掉一次登录,界面还停留在"已登录"样子

二、🔴 证据链(逐条实测)

① TTL 就是 24 小时

位置实测值
backend/app/config.py:18self.jwt_ttl_hours = int(os.getenv("JWT_TTL_HOURS", "24"))
backend/app/security.py:18"exp": utcnow() + timedelta(hours=settings.jwt_ttl_hours)
⚠️ 生产 /srv/kailash/.env⚠️ JWT_TTL_HOURS 未设置 ⇒ ⚠️ 实际就是默认 24 小时

② 静默续登完全可行,且后端零改动

事实证据
⚠️ 前端 loginWx() 打的是 POST /api/miniapp/auth/login,body {code}services/auth/login.ts:26
⚠️ 后端 _resolve_openid:⚠️ if req.code and settings.wx_app_secret: 才走 jscode2sessionbackend/app/routers/auth.py:47,50
⚠️ 生产 WX_APP_SECRET 已配置(长度 32)⚠️ 只查存在性,未读取值(遵守 secrets 红线)
⇒ 结论⭐ 续登只需 wx.login() 拿新 code 打同一端点,token 到手。后端不用改一行。

③ 续登不会误落进「体验/测试态」

_is_trial_openid 逻辑(auth.py:60-78):

⇒ 静默续登拿到的仍是同一 openid、同一 user、同一 token_version ⇒ 续登是安全的。

④ 登录态判断散落 5 处、各写各的

pages/profile/profile.ts        7 处
pages/order-detail/order-detail.ts  3 处
pages/login/login.ts            3 处
pages/item-detail/item-detail.ts     3 处
pages/feedback/feedback.ts      2 处

⚠️ 且口径不一:isLoggedIn()(services/auth/login.ts:35)= !!getToken()(只看存在,不看有效)

⚠️ ⚠️ vs this.data.logged(页面自己的 setData)⇒ 同一页面两种判断并存。

⑤ 401 没有任何全局广播

services/api/request.ts:92-94:

if (status === 401) {
  clearToken()          // ← 只清 token
  return reject(makeError('auth', 'UNAUTHORIZED', ...))
}

⚠️ 没有 emit / EventBus / 全局回调 ⇒ ⚠️ 页面完全不知道"登录已失效"

⚠️ ⇒ 每个页面各自 catch ⇒ 有的降级、有的弹 toast、有的静默

⚠️ ⇒ 这就是控制台一堆 401 + 未读数读取失败 的直接原因

⑥ 整个登录模块只有 37 行,且测试入口混在生产文件里

services/auth/login.ts 共 37 行,内含:

⑦ 附带发现:_resolve_openid 有降级路径

⚠️ 若 wx_app_secret 未配 或 code 无效,⚠️ _resolve_openid 会走降级分支

⚠️ ⇒ 可能产生 dev_/trial 类openid ⚠️ ⚠️ 这与记忆里「dev_*/test* 假 openid 被 /pay 升 502」是同一族问题


三、✅ 改造方案(四项,按依赖顺序)

P0-1 ·401 全局广播

改哪里:services/api/request.ts

新增:authEvents(极简事件表,⚠️ 不引三方库)
  · onSessionExpired(fn)   —— 订阅
  · emitSessionExpired()   —— 401 时触发(⚠️ ⚠️ ⚠️ **只触发一次**,⚠️ 防止并发请求重复弹)

在 401 分支里:先 emitSessionExpired(),再 clearToken()
⚠️ 必须去重:用户可能同时有 3 个请求返回 401,⚠️ **不能弹 3 次「请重新登录」**

⚠️ 配套:app.ts 或各页 onLoad 订阅一次 ⇒ 收到就 setData({logged:false})

⚠️ 这一步单独就能消掉「假登录态」。


P0-2 · 静默续登(⚠️ 客人无感)

改哪里:services/auth/login.ts + request.ts

新增 ensureFreshSession():
  ① 本地解析 token 的 exp(⚠️ JWT payload 本地可解,⚠️ 不需要请求)
  ② 若 exp 距今> 2 小时 ⇒ 直接用
  ③ 否则(或已过期)⇒ wx.login() → POST /api/miniapp/auth/login → 存新 token
  ④ 失败 ⇒ 走正常登录流程

⚠️ 在 request.ts 收到 401 时:先尝试 ensureFreshSession(),⚠️ 成功则**重放原请求一次**
⚠️ ⚠️ 只重放一次(⚠️ 防止无限循环)

⚠️ ⚠️ 关键安全点:⚠️ 续登请求本身不能再走 401 重试 ⇒ ⚠️ 否则死循环。

⚠️ ⇒ 必须给续登请求打「免重试」标记。


P1-1 · 登录态收口到一处

改哪里:新增 services/auth/session.ts

统一导出:
  · sessionState      —— 单一数据源
  · getLogged()       —— ⚠️ **判断「token 存在 且 未过期」**(⚠️ 不是 !!getToken())
  · onChange(fn)      —— 订阅变化
⚠️ 然后 5 个页面改为订阅它,⚠️ 不再各自 setData logged

⚠️ ⚠️ 这一项改动面最大(5 个页面)⇒ ⚠️ 我建议 P0 完成后单独一轮做,⚠️ 一次改 5 个页面风险偏高。


P1-2 · loginDev 从生产代码剥离

⚠️ 建议:移到 services/auth/login.dev.ts,⚠️ 仅在 __wxConfig.envVersion === 'develop' 时可引用。

⚠️ ⚠️ 但⚠️ 这会影响我自己的自动化探针 ⇒ ⚠️ 需确认探针是否还要用 dev 登录。


P2 · 登录到期倒计时(可选)

⚠️ getLogged() 已能读 exp ⇒ ⚠️ 可在「我的」页显示"登录还有 X 天到期"。


四、⚠️ 必须先确认的两件事(影响方案)

#问题为什么必须先定
①⚠️ TTL 要不要延长? 现在 24h。⚠️ 改成 7 天?30 天?⚠️ 延长 TTL 降低续登频率,⚠️ 但 ⚠️ 也降低安全性。⚠️ 这是产品决策,我不擅自改
②⚠️ loginDev 我的探针还要用吗?⚠️ 若要剥离 dev 登录,⚠️ 我的自动化验证会受影响 ⇒ ⚠️ 需要替代方案

五、⚠️ 我要做但必须说清的两件事

① 本次改动必须你上传小程序才生效

⚠️ 全部是 C 端 TS 改动。⚠️ 后端一行都不用改(⭐ 这是好消息)。

② ⚠️ 我不会碰 /srv/kailash/secrets/

⚠️ 本次审计我只查了环境变量的「存在性与长度」,⚠️ 从未读取任何密钥值。

⚠️ 沿用你 10-04 的口径:密钥目录只读不写、不移动、不轮换。


六、⚠️ 建议的执行顺序

第1 轮  P0-1 401 全局广播(改动最小,风险最低,单独就能解决「假登录态」)
        ↓ 真机验证:清 token 打开小程序 ⇒ 界面应立刻切未登录,且只弹一次提示
第 2 轮  P0-2 静默续登(⚠️ 需先定 TTL)
        ↓ 真机验证:改系统时间或用过期 token ⇒ 应自动续上,客人无感
第 3 轮  P1-1 登录态收口(⚠️ 5 个页面,⚠️ 建议单独一轮)
第 4 轮  P1-2 dev 登录隔离(⚠️ 需确认探针依赖)

⚠️ ⚠️ 每一轮都真机验证后再进下一轮 —— ⚠️ 不合并提交,

⚠️ 免得像这次底栏那样,一次改太多找不到是哪一处导致的。