3806 字
约 12 分钟
3
鉴权认的是谁

鉴权认的是谁

面向 VOZEB PRO。鉴权要练的不是登录页怎么排,而是:每一扇门怎么把凭证收成同一个问题——现在这个请求算谁。 08 说有两扇门;这篇说门上挂什么证件、服务端信哪一部分、什么时候绝不能混用。

先用一个全程例子把后文钉住。小王用浏览器登录,点「生成图片」,然后把页面关了。中间出现过三种「我是谁」,别混:

时刻 谁在敲门 手里的东西 服务端认成谁
打开登录页提交账号密码 小王的浏览器 账号 + 密码 还没有会话,先核凭证,核过才发手腕带
点生成、刷进度、看自己的图 小王的浏览器 Cookie vozeb_pro_session 小王(查会话行对上哈希)
页面关了,图还在出 generation-worker Bearer 维护令牌 +「这单是小王的」 先认「自己人在催单」,业务仍记在小王头上

顶栏 Zustand 里显示「小王」只是上次接口抄下来的名牌。真正放行的是上表中间那一列。


0. 什么时候用这套思路

先问三句,有一句对上就该掏出这篇,而不是先引一套 Passport / JWT 中间件。

  1. 敲门的是浏览器,还是自己家的进程? 证件不会是同一种。
  2. 浏览器里那串东西,服务端能不能在不查库的情况下信它说的 userId / role? 能,说明你做成了可伪造的声明,不是本仓的会话。
  3. 催单员拿到令牌之后,能不能随便填一个用户当自己? 能,说明维护身份和业务归属黏在一起了。

适合:

  • ✅ 新写一个要「当前用户」的 API,不知道该不该传 requestgetCurrentUser
  • ✅ 给 Worker / 定时扫库加维护入口,想复用登录 Cookie 或自己签一个 JWT
  • ✅ 前端想把 token 放进 localStorage / Zustand 当登录态
  • ✅ 登出只清了 React 状态,接口却还能用
  • ✅ 要把当前用户对象返回给浏览器,不确定哪些字段能出站
  • ✅ 读 StudyVault/02-鉴权与会话,或看到 vozeb_pro_session / VOZEB_PRO_MAINTENANCE_TOKEN

不要硬套:

  • ⚠️ 媒体短时 HMAC、预签名 S3(那是读路径的一次性门票,见 07,不是登录会话)
  • ⚠️ 渠道 API Key、支付密钥(服务端出站用,永不进浏览器)
  • ⚠️ 页面上「显示一下用户名」(那是 serializeCurrentUser 的展示副本,不是鉴权本身)
  • ⚠️ 纯公开页(画廊未登录可读的那部分)——没有「当前用户」就不要假装有

在 VOZEB PRO 中的定位: 用户进站靠不透明 Cookie vozeb_pro_session;Worker 进站靠环境变量维护令牌。getCurrentUser 把两种证明收成 PublicUser | null。没有独立 Auth 服务;会话行和用户行住在同一套库。


1. 核心公式

鉴权 = 两种进站凭证 → 一个「当前是谁」→ Route 才开门。

谁来 证件 证明了什么 没证明什么
浏览器 vozeb_pro_session = sessionId.token 这台浏览器持有一条未过期的服务端会话 用户是管理员、可以催别人的单
Worker Authorization: Bearer <维护令牌> 这是自己家的催单进程 令牌持有者等于某个超级用户

业务(扣积分、媒体主人、限额)永远认 用户行上的 user.id,不认「令牌很强所以随便指定一个人」。

饭店对照:顾客手腕带(Cookie)证明你订过桌;后厨内部对讲暗号(维护令牌)证明催菜的是自己人。催菜的人不能把暗号一亮就说「这桌算我的」。

例子:同一栋楼,两张通行证

小王进店,前台给他一只写着乱码的手腕带(Cookie)。收银、取菜都看这只带,不看他口头说「我是小王」。

厨房里的催菜铃是对讲机暗号(维护令牌)。暗号对了,才允许问「3 号桌那份牛排好了没」。催菜员不能靠暗号说「今晚所有桌算我请客」——账单还是 3 号桌小王的。


2. 什么场景用哪一种证件

  • 用户点的任何写操作:生成、保存提示词、改资料、下单
  • 用户读自己的私有媒体、会话、积分
  • 浏览器里的 services/api 默认就会带上 Cookie,不必再塞 Authorization

例子: 小王点「保存提示词」。POST /api/my-prompts 不会在 JSON 里再写一遍 token。浏览器自动带上 vozeb_pro_session。Route 里 getCurrentUser() 不传 request,认的就是这颗 Cookie。换成维护令牌打这个接口,应当 401——那不是催单门。

用维护令牌

  • generation-worker/api/maintenance/...
  • 进程内部需要「以维护身份替某用户把任务往前推」时,用 maintenanceWorkerHeaders / maintenanceWorkerContext,仍先校验令牌
  • 不要用它给浏览器做免登、不要把它写进前端环境变量

例子: 小王关了页,视频还在跑。Worker 每 2 秒 POST /api/maintenance/generation-tasks/run,头是 Authorization: Bearer <环境变量里那串>,再带 x-vozeb-pro-worker-user-id: 小王的id。门卫先核对令牌是不是自己人,再确认小王账号还是 active,然后才催小王那一行任务。若有人把令牌写进前端 .env、在浏览器里带 Bearer 打用户 API,那是把后厨暗号发给了顾客。

两种都不要、或另走一条

场景 走哪
展示已登录用户的图 Cookie(或 07 的短时媒体签名),不是维护令牌
上游来读一张参考图 媒体 HMAC;download=original 不能升级成匿名原件
登录本身 限流 + 校验账号密码,然后才 createSession
完全公开的作品页 单独的公开入口和限流,不要把私人 Route 改成「有链接就行」

例子: 小王把生成结果插进 Canvas。节点里存的是 /api/reference-assets/permanent/2026/08/16/images/....webp,不是维护令牌,也不是登录 Cookie 的拷贝。打开画布时浏览器仍靠会话 Cookie(或一张短时媒体签名)去拉图。签名过期了应再签一张,不要改成「把维护令牌拼进图片 URL」。


3. 用户证件:不透明会话,不是 JWT

登录成功只做这一件事:造一条服务端说了算的会话,把浏览器看不懂的值写进 Cookie。

凭证 → authenticateUser → createSession(userId)
     → 库里只存 token 的哈希
     → Set-Cookie: vozeb_pro_session=sessionId.token
       httpOnly + sameSite=lax +(HTTPS 时)secure

例子:小王登录成功后手里到底有什么

服务端新开一行会话,比如 id 是 sess_aaa。真正的密码段是 tok_bbb(随机串)。

  • 写进 Cookie、发给浏览器的是:sess_aaa.tok_bbb
  • 写进数据库的是:sess_aaa + hash(tok_bbb) + 过期时间

小王用开发者工具看见 Cookie,也解不出「我是 user_123、我是 admin」。他若把 tok_bbb 改成 tok_hack,下次请求哈希对不上,直接当没登录。管理员在后台删掉 sess_aaa 这一行,小王立刻全站退出,不必等「token 七天后再过期」。

之后每个用户请求:

Cookie → getUserBySession
      → 有这一行?哈希对得上?没过期?
      → PublicUser 或 null

为什么要这么拧:

设计 防的是什么
不透明(浏览器解不出 userId/role) 改 Cookie 自称管理员
sessionId 找行,token 当密码,库里只存哈希 库被拖走不能直接拿去登录
每个请求查会话行 登出 / 封号 / 删会话立刻失效,不必等 JWT 过期
httpOnly XSS 不能 document.cookie 把会话偷走
sameSite=lax 别的站点不能随便带着这颗 Cookie 打 POST
secure 看真实协议(反代时看转发头,且必须先配可信跳数) 明文 HTTP 把会话打出去

前端 Zustand 里的当前用户不是登录态。 那是上次接口返回的展示副本。权威在 Cookie + 库。登出 = deleteSession + 清 Cookie,不是 user = null

例子:只清前端会怎样

小王点退出,若页面只执行 useUserStore.setState({ user: null }),顶栏看起来是游客,但 Cookie 还在。他再点一次生成,getCurrentUser() 仍能从 Cookie 找回小王,单照开、积分照扣。正确登出必须让库里的 sess_aaa 消失,并且 Set-Cookievozeb_pro_session 清掉。

返回给浏览器的 serializeCurrentUser 是裁过的:id、名字、积分、套餐可以有;上游 Base URL、SMTP、支付密钥、Skill 指令不能有。公开设置同理。

例子:回包里多带了什么叫泄密

可以给前端:{ id, displayName, pointsBalance, planName }
不能给前端:{ openaiKey, smtpPass, channelBaseUrl, skillInstructions }
后者属于服务端出站,进了浏览器就等于把后厨钥匙挂在大厅告示牌上。


4. 催单证件:维护令牌 + 落到真实用户

Worker 没有浏览器,不能「先登录再带 Cookie」。证件是环境变量 VOZEB_PRO_MAINTENANCE_TOKEN(至少 32 字符)。

maintenance-auth.ts 的规矩:

  1. Authorization: Bearer ... 与配置令牌做时间安全比较(防按响应时间猜令牌)
  2. 对上了,才算维护请求
  3. 可选头 x-vozeb-pro-worker-user-id:这张单在替谁往前推
  4. 用这个 id 去取用户,必须存在且 active,否则仍当没人

例子:令牌对了也不等于想当谁就当谁

Worker 带了正确的 Bearer,但把头里的 userId 改成「管理员小李」或一个已封禁账号。门卫应当:令牌这一关通过,取用户这一关失败或拒绝当 admin 冒充,不催、不扣小李的积分。正确头是当初开单的小王,且小王仍是 active。

getCurrentUser(request?) 的顺序:

先看 Cookie → 有合法会话就用会话用户
否则若传入了 request → 再看维护令牌 + worker userId
否则 null

不传 request 就只认 Cookie。 维护 Route 若忘了传,Worker 永远是路人。用户 Route 不要传一个不可信的 Request 就指望「顺便」接受令牌——维护入口应当显式、收口。

例子:忘了传 request

维护 Route 里写成 getCurrentUser()。Worker 请求没有 Cookie,函数看完 Cookie 就是 null,直接 401。Compose 日志会像「Worker 在打,业务说没登录」。补上 getCurrentUser(request),或先 isAuthorizedMaintenanceRequest(request),催单才认得出自己人。

反过来:普通的 /api/image-tasks 若「顺便」接受 Bearer 维护令牌,浏览器或脚本就能绕过小王登录、用暗号开单。所以用户门只认 Cookie,催单门只认令牌,两扇门不要开条暗道。

令牌证明的是「自己人在催单」,不是「超级管理员想当谁就当谁」。扣积分、写 storageKey 主人、限额,仍认头里那个已存在且未封禁的用户。

进程内继续往下传时,用令牌 HMAC 签的 maintenanceWorkerContext,不要伪造一颗用户 Cookie 给自己。


5. 和「JWT 放 localStorage」差在哪

本仓库 常见 JWT 放前端
浏览器拿着什么 看不懂的 sessionId.token 自己能解码的声明
服务端存什么 哈希 + 过期 往往只验签名
踢人下线 删会话行立刻失效 到期前很难作废
Worker 单独的长令牌 经常硬塞一个「系统 JWT」
XSS httpOnly 读不到 JS 能读就能被偷

代价是每个请求查一次会话。对本单体可接受:会话本来就在同一套库,不必为了「无状态」把可作废性丢掉。

不要为了「前后端分离一点」把会话搬进 localStorage。那是在拆 08 的开单门证件,不是在现代化。

例子:JWT 放 localStorage 会怎样

若登录后把 { userId: 小王, role: user, exp: 七天后 } 塞进 localStorage,页面 JS 能读,XSS 也能读,偷走就能冒充小王直到过期。小王在别的电脑点退出,这台电脑上的 JWT 还活着。本仓的做法是:浏览器只持有看不懂的 Cookie,退出删的是库里那一行,所有电脑一起失效。


6. Route 里怎么落地

用户 API:

const user = await getCurrentUser(); // 不传 request
if (!user) return 401;
// 之后只认 user.id

维护 API:

if (!isAuthorizedMaintenanceRequest(request)) return 401;
// 或 getCurrentUser(request),且必须把 request 传入
await runXxxBatch(...);

登录 Route:先限流,再校验凭证,再 createSession + setSessionCookie。限流是「打太勤」,会话是「你是谁」,两步不要合成一步。

readJsonBody 只管 body 干净,不管你是谁。

例子:两段代码分别在防什么

小王打 POST /api/image-tasks,body 是 { prompt: "一只猫", clientRequestId: "..." }

  • getCurrentUser() 回答:这是小王,还是匿名——你是谁
  • readJsonBody 回答:JSON 能不能解析——你说了什么
  • 限流回答:小王这一分钟是不是点了太多次——打得勤不勤

登录接口也一样:先限流防有人拿脚本撞密码,再核账号密码,最后才发 Cookie。不能「密码对了就不限流」,也不能「限流过了就不核密码」。


7. 写代码时的五问

  1. 这个入口的来客是浏览器还是自家进程?证件有没有用错扇门的那一种?
  2. 调用 getCurrentUser 时传不传 request?维护入口忘了传,等于没鉴权。
  3. 浏览器能不能在不经过服务端的情况下,改手里的字符串就换一个 userId / role?
  4. 登出有没有删会话行?只清 Zustand 算没做。
  5. 返回给前端的用户 / 设置对象里,有没有上游地址、密钥、Skill 指令?

8. 读完能指挥自己(或 AI)做什么

  • 「用户 API 只认 Cookie:getCurrentUser() 不传 request;没有人就 401。」
  • 「维护 API 验 VOZEB_PRO_MAINTENANCE_TOKEN;用户落到 header 里那个 active 的 userId,禁止令牌直接当 admin。」
  • 「会话不透明,库里只存 token 哈希;不要 JWT 进 localStorage。」
  • 「登出删会话 + 清 vozeb_pro_session,不要只清前端 store。」
  • serializeCurrentUser / 公开设置不得带上游、SMTP、支付密钥、Skill 指令。」

相关文档

鉴权认的是谁
http://clxhxhhr.top/posts/531/
作者
clxstart
发布于
2026-09-08
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。