审计时间:2026-10-12 11:46–11:52
触发:义父「登录系统是很关键的,容不得瑕疵,你一定要仔细分析一下」
⚠️ 本报告全部结论均来自代码与生产配置实测,无推测
⚠️ 核心问题一句话:
JWT 有效期 24 小时 + 没有静默续登 + 401 无全局通知
= 客人每天打开小程序都会掉一次登录,界面还停留在"已登录"样子
| 位置 | 实测值 |
|---|---|
backend/app/config.py:18 | self.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: 才走 jscode2session | backend/app/routers/auth.py:47,50 |
⚠️ 生产 WX_APP_SECRET 已配置(长度 32) | ⚠️ 只查存在性,未读取值(遵守 secrets 红线) |
| ⇒ 结论 | ⭐ 续登只需 wx.login() 拿新 code 打同一端点,token 到手。后端不用改一行。 |
_is_trial_openid 逻辑(auth.py:60-78):
dev_ 前缀 ⇒ 直接 False(那是开发登录,与真实微信无关)trial_openids 白名单命中 或 trial_auto_accept='true' 时才算 trialFalse(不阻塞登录)⇒ 静默续登拿到的仍是同一 openid、同一 user、同一 token_version ⇒ 续登是安全的。
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)⇒ 同一页面两种判断并存。
services/api/request.ts:92-94:
if (status === 401) {
clearToken() // ← 只清 token
return reject(makeError('auth', 'UNAUTHORIZED', ...))
}
⚠️ 没有 emit / EventBus / 全局回调 ⇒ ⚠️ 页面完全不知道"登录已失效"
⚠️ ⇒ 每个页面各自 catch ⇒ 有的降级、有的弹 toast、有的静默
⚠️ ⇒ 这就是控制台一堆 401 + 未读数读取失败 的直接原因
services/auth/login.ts 共 37 行,内含:
loginDev(devId) ⚠️ 开发登录loginWx() ⚠️ 生产登录dev_* 后门**_resolve_openid 有降级路径⚠️ 若 wx_app_secret 未配 或 code 无效,⚠️ _resolve_openid 会走降级分支
⚠️ ⇒ 可能产生 dev_/trial 类openid ⚠️ ⚠️ 这与记忆里「dev_*/test* 假 openid 被 /pay 升 502」是同一族问题
改哪里:services/api/request.ts
新增:authEvents(极简事件表,⚠️ 不引三方库)
· onSessionExpired(fn) —— 订阅
· emitSessionExpired() —— 401 时触发(⚠️ ⚠️ ⚠️ **只触发一次**,⚠️ 防止并发请求重复弹)
在 401 分支里:先 emitSessionExpired(),再 clearToken()
⚠️ 必须去重:用户可能同时有 3 个请求返回 401,⚠️ **不能弹 3 次「请重新登录」**
⚠️ 配套:app.ts 或各页 onLoad 订阅一次 ⇒ 收到就 setData({logged:false})
⚠️ 这一步单独就能消掉「假登录态」。
改哪里: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 重试 ⇒ ⚠️ 否则死循环。
⚠️ ⇒ 必须给续登请求打「免重试」标记。
改哪里:新增 services/auth/session.ts
统一导出:
· sessionState —— 单一数据源
· getLogged() —— ⚠️ **判断「token 存在 且 未过期」**(⚠️ 不是 !!getToken())
· onChange(fn) —— 订阅变化
⚠️ 然后 5 个页面改为订阅它,⚠️ 不再各自 setData logged
⚠️ ⚠️ 这一项改动面最大(5 个页面)⇒ ⚠️ 我建议 P0 完成后单独一轮做,⚠️ 一次改 5 个页面风险偏高。
loginDev 从生产代码剥离⚠️ 建议:移到 services/auth/login.dev.ts,⚠️ 仅在 __wxConfig.envVersion === 'develop' 时可引用。
⚠️ ⚠️ 但⚠️ 这会影响我自己的自动化探针 ⇒ ⚠️ 需确认探针是否还要用 dev 登录。
⚠️ 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 登录隔离(⚠️ 需确认探针依赖)
⚠️ ⚠️ 每一轮都真机验证后再进下一轮 —— ⚠️ 不合并提交,
⚠️ 免得像这次底栏那样,一次改太多找不到是哪一处导致的。