【项目实战】第7篇:登录鉴权与数据库教程——「我是谁」卡了我一整天

博客文章#Workbuddy阅读 0
【项目实战】第7篇:登录鉴权与数据库教程——「我是谁」卡了我一整天

系列:《用 AI 写出高质量代码:AI 狼人杀开发实录》
本篇对应版本:v1.9.3 → v1.9.8|时间:2026-10-23

前言

前六篇做的都是「玩起来」的事。这一篇开始做「留下来」的事:战绩要记住、身份要认得、朋友要能找到。

这些需求交给 AI 做,看起来都不难——登录它有 SDK,战绩它有云库。做完之后功能演示也一切正常。

直到我第一次用真实视角把它用了一遍:点了「获取验证码」,页面毫无反应。 而它给出的解释永远是「没登录」,且不告诉你为什么。


引子:最难查的 bug,是「什么都没发生」

当时我撞的不是一个 bug,是一串。它们表现形式完全一样:没有一处报错。

  • 输入手机号,点「获取验证码」——不转圈、不提示、什么都不发生
  • 换个办法终于登录了,刷新一下,「我的战绩」说「登录态已失效」
  • 服务端日志一切正常,可它就是认不出我是谁

报错会告诉你去哪查。静默降级不会——它只是把你当成匿名用户,然后一切照常运行。

这就是「身份」真正难的地方:它不是加一个按钮,而是一条从浏览器到数据库的链路,而这条链上有好几处会悄悄把你换成别人。这篇文章按「链上的四处替换」来讲——每一处的找法、改法,以及事后怎么让 AI 不再犯。


问题原因:这条链上有四处会把你换掉

图:这条链上有三处会把你「换成别人」,链外还有一处(构建产物)。四处都不报错,你看到的都是同一句「登录不上去」。

身份这件事,从浏览器到数据库要经过六步(上图)。这条链任何一环断了,你看到的都是同一句「登录不上去」——因为每一环的设计都是「认不出就当你是匿名」,没有一环会报错。


解决方案:四处替换,四次比对

下面按图上 ①②③④ 的顺序逐处来:每一处都是现象 → 怎么定位 → 根因 → 改法 → 沉淀成规则。这是这篇真正的主线——发现靠人,动手靠 AI,验证靠测试。

① 字段被换:SDK 认的是实现,不是注释

现象:点「获取验证码」,不转圈、不报错、什么都不发生。 定位:日志没错误、请求也发出去了,让 AI 直接读 node_modules 里的运行时实现,看它到底认哪个字段。 根因:类型声明(.d.ts)的注释写的是 phone_number,运行时只读 credentials.phone;两个都不传时 SDK 返回一句 sendOtp requires exactly one of email or phone,而这个错误被前端的 catch 吞了,于是「什么都没发生」。 改法:照运行时实现传字段——sdk.sendOtp({ phone })(对)而不是 sdk.sendOtp({ phone_number: phone })(错)。

沉淀:把这条写进 AGENTS.md 的硬性禁止——

禁止凭 .d.ts 注释使用第三方 SDK 字段。必须读运行时实现:sendOtp 只认 {phone} / {email},传 phone_number 会静默失败。

② 请求头被换:你发的 Authorization 不是服务端收到的

现象:登录成功,刷新后提示「登录态已失效」——和下一节③的症状一模一样。这就是本篇最需要注意的地方:同一个症状,两个完全不同的病因。

定位:这一处读代码找不出来,只能比对。让 AI 在服务端把实际收到的 token 算 SHA-256、取前 8 位打出来,和前端发出的 token 并排看。结果:对不上,而且每个请求都不一样。

根因:平台网关用自己的凭据覆盖了 Authorization 头。也就是说,用户身份从来没到过服务端,账号闭环自 v1.5 起实际上一直是失效的——只是它表现成「静默降级」,所以没人发现。

改法:换一个网关不会碰的自定义头。让 AI 把读凭证的逻辑改成优先 X-WW-Token:

// 服务端:优先自定义头,仍兼容 Authorization(本地 curl 调试方便)
const token = req.headers['x-ww-token'] ?? bearer(req.headers.authorization);

前端两个头都带,服务端以 X-WW-Token 为准。实测自定义头原样透传,哈希能对上。

③ 会话被换:少了 Origin,你就是另一个客户端

现象:跟 ② 一模一样——登录着,但服务端认不出你。

定位方法:命令行直接调云端的 /user/me,看它回什么:

HTTP 401  {"error":"invalid_grant","error_description":"the session was issued for another client"}

根因:少了 Origin,网关把它当成另一个客户端,直接 401;ownerId 永远是 null,静默降级成匿名。

改法:让 AI 把服务端请求补上 Origin 和 Referer,值等于应用的公网域名。

④ 产物被换:你测的和线上跑的不是同一份

现象:代码改对了、本地跑通了,线上还是老样子。 定位方法:先排除缓存(清缓存、无痕、换手机)——全都无效,那就不是浏览器的问题,是服务端发的东西没变:比对线上引用的 JS hash 和本地刚构建的产物。

根因:部署脚本为了「省时间」,看到 dist/ 存在就跳过构建(产物齐了就不重建),而沙箱目录是持久的,dist/ 会原样留到下一次发布,线上一直在发旧包。

改法:让 AI 改成每次启动都重建;构建失败但旧产物还在时降级沿用(宁可页面旧,也不能 504)。


一条纪律:给链路装一个观测点

四处查完,我意识到这四个坑能藏那么久,有一个共同原因:每次排查都要把「这个值在哪一段被换了」重新推理一遍。 所以让 AI 加了一个很小的接口 GET /api/whoami,它只回四件事:认没认出你(signedIn)、为什么认不出(reason)、云端返回什么状态码、以及你实际收到的 token 的 SHA-256 前 8 位(tokenHint)。

它把原本要折腾几小时的排查压成了几秒。前端也复用它:战绩页认成匿名时,直接把原因写到界面上,而不是给你一个 0 局。


效果:从几小时到几秒

链路通了的直接证据,就是上面那个 whoami:从「不知道」变成「认得出 / 认不出 + 原因」。

然后是界面上看得见的变化:

图:v1.9.5 的「我的战绩」。未登录时不是空白,是一句说明加一个入口;登录后这里就是战绩、胜率、进行中的局。

图:v1.9.5 的登录页(从当时的提交跑出来的)。三种方式、字段提示、错误提示放在按钮下方——这些都是踩完坑补的。


内容小结

  1. 身份是一条链路,不是一颗按钮:输入 → token → 请求头 → 服务端解析 → 数据库。断哪一环,表现都是「登不上」。
  2. 静默降级比报错难查:认不出就匿名,程序照跑。先让失败可见,再谈修。
  3. 排查靠比对,不靠猜:每个边界各打一次值,把「发出的」和「收到的」并排看——四处问题都是这么定下来的。

参考资料

  • 项目仓库:src/server/auth.ts(读凭证)、src/server/identity.ts(认人)、src/cloud/social-store.ts、client/src/auth/actions.ts
  • 相关测试:tests/auth-actions.test.ts、tests/whoami.test.ts、tests/auth-config.test.ts
  • 项目规则:AGENTS.md §12 硬性禁止(每条对应一次真实事故)、docs/standard/SECURITY.md
  • 上一篇:第6篇:UI 设计教程——一眼就是「AI 做的」
  • 下一篇:第8篇:分享链接与 URL 教程——发出去的链接,朋友打不开

[评论 0]

评论加载中…