Files
Volcano-Engine-TTS-UI/docs/IMPLEMENT_v0.3.0.md
T
sun 94b3557c7e docs: v0.2.4 阶段三 文档、示例配置与发布说明
文档与配置同步 v0.3.0 的破坏性变更:

README.md:
- 特性列表更新:管理入口 /dashboard、凭证分离、默认无匿名监控端点
- 环境变量表补充 METRICS_ALLOW_CIDR,并说明未配任何管理凭证时服务拒绝启动
- 端点文档重写:/healthz(匿名且不含字段)、/health(需鉴权)、
  /metrics(默认 404,两种使用方式)、管理页面与安装页对照表
  (含"页面需鉴权为何又能直接打开"的内容协商说明)
- 公网部署安全清单重写:按端点列出真实暴露面,并提醒探针必须指向 /healthz
- 故障排查、架构表、迁移表中的 /admin 引用统一改为 /dashboard

.env.example:新增 METRICS_ALLOW_CIDR 与 admin_key 说明(含 Docker 网段警告)。

两处部署陷阱修复(阶段一给 /health 加鉴权引发的连带问题):
- docker-compose.yml 的 healthcheck 原指向 /health,加鉴权后会一律 401,
  导致容器被判定为 unhealthy → 改为 /healthz
- Dockerfile 的 HEALTHCHECK 同样指向 /health → 改为 /healthz
  (这两处若不改,升级后容器健康状态会持续异常,且现象与鉴权改动看起来无关,排查成本高)

新增 docs/RELEASE_NOTES_v0.3.0.md:可直接用于创建 Release,
含 Breaking Change 清单、新特性、验证方式与实测结果、升级步骤。

docs/IMPLEMENT_v0.3.0.md:
- 按合并后的阶段编号重写实施进度表与阶段标题
  (原「阶段 1 收紧鉴权」与「阶段 2 收口端点」合并为「阶段 1」,后续顺延),
  并说明合并原因:两者只做前者是"有风险无收益"
- 记录本轮实际验证结果

验证:go build / go vet / go test ./... -count=1 全绿。
2026-10-04 01:17:43 +08:00

23 KiB
Raw Permalink Blame History

Volcano-Engine-TTS-UI v0.3.0 阶段性实施步骤书

文件名:IMPLEMENT_v0.3.0.md 基线:develop 版本目标:v0.3.0 —— 路由鉴权架构升级、健康/指标端点收口、管理入口统一到 /dashboard 关联文档:UPSTREAM_ADAPTER_GUIDE.md(上游适配器重构,与本改造正交,互不干扰)

本文是 v0.3.0 的唯一实施依据,由初版方案按实际代码基线校正而成。 校正说明见 附录 A。


实施进度

编号说明:初版方案的"阶段 1(收紧鉴权)"与"阶段 2(收口端点)"属于同一次安全改造、 必须一起上线才有意义(只做前者是"有风险无收益"),已合并为现在的「阶段 1」, 后续阶段依次顺延。下表的阶段号均为合并后编号。

阶段 内容 状态 提交
1 收紧 RequireAdmin + 独立 admin_key + 启动期校验 + 收口健康/指标端点 + /healthz + METRICS_ALLOW_CIDR ✅ 已完成 见下
2 路由分层 + 旧路径别名 + /admin→/dashboard ✅ 已完成 见下
3 文档 / 示例配置 / Docker 注释 / CHANGELOG ✅ 已完成 见下
4 全量回归测试(发布前) ⏳ 待执行 —

阶段 3 额外修复的两处部署陷阱(阶段 1 给 /health 加鉴权引发的连带问题):

  • docker-compose.yml 的 healthcheck 原指向 /health;该端点加鉴权后会一律 401, 导致容器被判定为 unhealthy → 已改为 /healthz
  • Dockerfile 的 HEALTHCHECK 同样指向 /health → 已改为 /healthz

0. 改造目标

# 目标 验收标准
1 收口健康/指标端点:/metrics、/health、/dashboard 不再匿名可读 未携带管理凭证时不可读到任何健康数据
2 补前置缺口:RequireAdmin 在凭证未配置时不再静默放行 未配置凭证时管理接口拒绝访问,且启动期有明确信号
3 权限隔离:业务调用凭证不得访问管理接口 用 TTS 调用 key 访问 /api/admin/* 返回 401
4 管理入口统一:/admin → /dashboard,旧路径 301 兼容 访问 /admin 301 到 /dashboard
5 API 路由分层:/api/public/*、/api/user/*、/api/admin/* 三条前缀各自挂对应中间件,/api/user/* 为桩
6 存量管理接口迁移到 /api/admin/*,旧路径继续可用 新旧路径行为一致,老脚本零改动
7 两套用量查询占位接口 /api/user/usage(管理鉴权)、/v1/dashboard/billing/usage(Bearer)
8 文档 / 示例配置 / Docker 注释同步更新,标注 Breaking Change README、.env.example、docker-compose.yml、CHANGELOG 一致
9 不改动 adapter/、store/ 核心业务模型,不破坏 /v1/audio/speech TTS 业务接口回归通过

重要约束

  • v0.3.0 仍是单用户模式;/api/user/* 仅骨架 + 桩实现,完整多用户能力放 v0.4.0。
  • 不引入 Cookie 会话(理由见 附录 B)。管理鉴权继续使用 Authorization: Bearer <key>,前端继续用 sessionStorage 保存凭证。
  • 每个阶段结束必须可编译、可运行、可本地回归,拒绝大爆炸式一次性修改。

1. 现状基线(开工前必须知道的事实)

本项目的真实鉴权现状与初版方案的假设不同,照着初版做会返工。以下是核对过代码的结论:

事项 真实现状 代码位置
管理鉴权中间件 只有 RequireAdmin,Bearer 请求头校验。不存在 AdminSessionMiddleware middleware/admin_auth.go:20
凭证存储 前端 sessionStorage['ttsAdminKey'],无 Cookie router/admin-shell.js:10-15
凭证来源 数据库 auth_key → 回退环境变量 OPENAI_TTS_API_KEY setting/config.go:384-392
凭证复用 同一个 key 既登录后台、又调 TTS 接口(本次要拆开) middleware/auth.go + middleware/admin_auth.go
指标实现 自研 telemetry 包,未引入 prometheus/client_golang,无 promhttp router/router.go:136
客户端 IP 已有 middleware.GetClientIP,支持 XFF + TRUSTED_PROXY_HOPS middleware/ratelimit.go:196
CORS 非白名单 Origin 在中间件层直接 403 middleware/cors.go:138-147

1.1 匿名可读端点(本次要收口的全部)

端点 泄漏内容 代码位置
GET /dashboard HTML 看板,同时拉 /health + /metrics,聚合度最高 router/router.go:120
GET /metrics Prometheus 全量指标(流量、模型名、错误分布、计费字符数) router/router.go:136
GET /health 版本 / commit / 运行时长 / 内存 / goroutine + 配置错误文本 router/router.go:119
GET /api/setup/status normal 模式下仍返回 {installed, mode} router/router.go:75

/api/setup/prefill 在 normal 模式返回 404(handler 内自检),不可达,无需处理。

1.2 已有但必须保留的机制

  • InstallGuard:安装模式下按白名单放行(/setup、/api/setup、/health、/metrics), 安装阶段无数据库、无凭证,不得套用 RequireAdmin。middleware/installguard.go:19
  • 现有 /api/admin/metrics 已有 RequireAdmin,返回同样的 Prometheus 文本(保留复用)。
  • 现有静态资源路由 /admin/admin.css、/admin/admin-shell.js(router/router.go:90-97), 迁移时必须同步处理,否则页面丢样式。

2. 分阶段实施

阶段 1|鉴权收紧 + 健康/指标端点收口(本版本核心)

本节由初版方案的「阶段 1(收紧 RequireAdmin)」与「阶段 2(收口端点)」合并而成。 合并原因:两者是同一件事的两半 —— 只做前者(收紧空凭证放行)而不同时收口端点, 结果是"有风险无收益"(旧部署可能起不来,而 /health、/dashboard、/metrics 仍匿名可读)。 下面 1.1–1.3 对应初版的阶段 1,1.4–1.6 对应初版的阶段 2。

为什么先做这一步:RequireAdmin 当前在凭证列表为空时直接放行(middleware/admin_auth.go:29-33)。 不修这个,后面给 /metrics、/health 套 RequireAdmin 等于没套。

改动范围:middleware/admin_auth.go、middleware/auth.go、setting/config.go、main.go

1.1 空凭证不再放行

将 len(keys) == 0 → next.ServeHTTP 改为拒绝 + 显式日志:

keys := setting.GetAdminKeys()
if len(keys) == 0 {
    log.Printf("[admin_auth] 拒绝访问:未配置管理凭证(admin_key / auth_key 均为空) - 路径=%s", r.URL.Path)
    denyAdmin(w, r)
    return
}

1.2 新增独立 admin_key(权限隔离)

  • store:新增可选的设置项 admin_key(无需数据库迁移,settings 是键值表)。

  • setting:新增 GetAdminKeys() []string,取值优先级:

    admin_key(DB) → 回退 auth_key(DB) → 回退 OPENAI_TTS_API_KEY(env)
    
  • RequireAdmin 改用 GetAdminKeys();middleware.ValidateAPIKey(/v1/audio/speech 用)保持只看 auth_key。

  • 效果:配了 admin_key 后,业务调用凭证无法访问管理接口,隔离成立;不配则行为与旧版完全一致(向后兼容)。

1.3 启动期校验(fail-fast)

在 main.go normal 模式分支加入:管理凭证为空时拒绝启动并打印清晰原因 (与现有配置损坏 fail-fast 风格一致)。

⚠️ 破坏性提示:若你的部署目前未配置任何凭证,本阶段后服务将无法启动。 这是刻意的——此前那种状态下管理接口是完全裸奔的。请先配好 auth_key(或 admin_key)再升级。

1.4 ✅ 鉴权部分回归

  • go build ./... && go vet ./... 通过
  • 配置 auth_key 后:/api/admin/overview 带正确 key 返回 200,错误 key 返回 401
  • 不配置任何凭证时启动:进程拒绝启动并给出明确日志
  • 配置独立 admin_key 后:用 auth_key 访问 /api/admin/* 返回 401
  • /v1/audio/speech 用 auth_key 调用不受影响

1.5 端点收口(本版本核心目标)

改动范围:router/router.go、controller/tts.go、middleware/(新增 CIDR 白名单)、setting/config.go

1.5.1 /health 拆分为两个端点

端点 鉴权 返回
GET /healthz 无鉴权 仅 200 + 字面量 ok,不含任何字段
GET /health RequireAdmin 现有完整 JSON(版本/内存/配置状态等)

⚠️ /healthz 必须与 /health 鉴权在同一次改动内落地。分开部署会出现探针空窗, 导致 K8s liveness 失败、Pod 反复重启。

📌 探针迁移提醒:若现有 K8s livenessProbe / readinessProbe / Docker HEALTHCHECK 指向 /health,需同步改指 /healthz。README 中要写明。

1.5.2 /metrics 收口 + 可选内网白名单

新环境变量 METRICS_ALLOW_CIDR(逗号分隔多个 CIDR):

  • 已配置:在根路径注册 /metrics,包裹 MetricsIPAllowListMiddleware; 非白名单 IP 返回 404(不返回 403,避免向扫描者确认端点存在)。
  • 未配置:根路径 /metrics 完全不注册,访问 404。
  • 两种情况都始终提供鉴权版 GET /api/admin/metrics(已存在,供面板使用)。

中间件实现要点:复用 middleware.GetClientIP(r)(已处理 XFF / TRUSTED_PROXY_HOPS), 用标准库 net/netip 解析 CIDR。

⚠️ Docker 环境:METRICS_ALLOW_CIDR 必须填容器内网网段(如 172.16.0.0/12), 不要填 127.0.0.1/32 —— 那是容器自身的回环,Prometheus 在宿主机或其他容器里进不来。

1.5.3 /dashboard 与 /api/setup/status 收口

  • /dashboard:套 RequireAdmin。它是本版本单点泄漏最严重的端点(一个 URL 暴露全部健康数据)。
  • /api/setup/status:套 RequireAdmin。
  • 前端状态提示:/dashboard 的 HTML 外壳本身不敏感,前端登录判断逻辑保持现状 (sessionStorage 无 key → 显示登录视图)。不在服务端做页面级跳转,原因见 附录 B。

1.6 ✅ 端点收口回归

  • 未配置 METRICS_ALLOW_CIDR:根 /metrics、/health 返回 404
  • 配置 METRICS_ALLOW_CIDR=127.0.0.1/32:本机可访问根 /metrics,其他 IP 返回 404
  • /healthz 无鉴权可访问且不含任何字段
  • /health 无凭证返回 401,带管理凭证返回完整 JSON
  • /dashboard 无凭证返回 401
  • /api/setup/status 无凭证返回 401
  • 面板内 /api/admin/metrics、/api/admin/health 正常返回

阶段 2|路由分层与路径迁移

改动范围:router/router.go、controller/(新增两个桩 handler + 页面路由别名)

2.1 分层路由注册

apiPublic := r.PathPrefix("/api/public").Subrouter()
// 预留空壳,暂不注册业务

apiUser := r.PathPrefix("/api/user").Subrouter()
apiUser.Use(middleware.RequireAdmin)   // 单用户模式下与 admin 等价,v0.4.0 再分化

apiAdmin := r.PathPrefix("/api/admin").Subrouter()
apiAdmin.Use(middleware.RequireAdmin)

将现有管理 handler 挂到 apiAdmin(复用 handler 本体,不改业务逻辑):

旧路径 新路径
GET/POST /api/voices GET/POST /api/admin/voices
DELETE /api/voices/{name} DELETE /api/admin/voices/{name}
PATCH /api/voices/{name}/toggle PATCH /api/admin/voices/{name}/toggle
GET/PUT /api/settings GET/PUT /api/admin/settings
PUT /api/settings/api-key PUT /api/admin/settings/api-key
PUT /api/settings/auth-key PUT /api/admin/settings/auth-key
PUT /api/settings/cors PUT /api/admin/settings/cors
GET /api/admin/overview 位置不变
GET /api/admin/metrics 位置不变

2.2 旧路径兼容:用别名,不要重定向

旧路径指向同一个 handler,不做 301/308 跳转。

理由(三者对比):

方案 问题
301 会把 POST/PUT/PATCH/DELETE 降级成 GET(RFC 7231),且浏览器永久缓存,客户端察觉不到路径变化
308 保留方法和请求体,比 301 好;但多一次往返,对非浏览器脚本仍是行为变化
别名(采用) 两个路径直接可用,零往返、零破坏,老脚本完全无感

旧路径在当前版本保留,并在代码注释标注"过渡期别名,未来版本移除"。

2.3 页面路由迁移

// 新入口
r.HandleFunc("/dashboard", middleware.RequireAdmin(serveAdmin(dashboardHTML))).Methods("GET")
r.HandleFunc("/dashboard/login", serveAdmin(adminLoginHTML)).Methods("GET")
r.HandleFunc("/dashboard/voices", middleware.RequireAdmin(serveAdmin(adminVoicesHTML))).Methods("GET")
r.HandleFunc("/dashboard/settings", middleware.RequireAdmin(serveAdmin(adminSettingsHTML))).Methods("GET")

// 静态资源:必须同步迁移,否则面板丢样式/脚本
r.HandleFunc("/dashboard/admin.css", ...)      // 原 /admin/admin.css
r.HandleFunc("/dashboard/admin-shell.js", ...) // 原 /admin/admin-shell.js

// 旧入口 301 永久重定向(页面用 301 正确)
r.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r, "/dashboard", http.StatusMovedPermanently)
}).Methods("GET")

前端同步改动(router/*.html、admin-shell.js):

  • 页面间跳转与链接:/admin → /dashboard,/admin/login → /dashboard/login 等
  • API 调用地址迁移到 /api/admin/*
  • 静态资源引用 /admin/admin.css → /dashboard/admin.css(同理 admin-shell.js)
  • admin-shell.js 的 401 拦截逻辑改为跳 /dashboard/login

📌 注意:/dashboard 原本是服务状态预览页(router/health.html)。迁入后该页面的定位 变成"管理后台首页",健康/指标数据改由 /api/admin/health、/api/admin/metrics 提供。 若仍希望保留独立的只读状态页,请单独确认(本版本不提供匿名版本)。

2.4 桩接口

端点 鉴权 行为
GET /api/user/usage RequireAdmin 单用户模式返回全局用量统计(桩,v0.4.0 改为按用户过滤)
GET /v1/dashboard/billing/usage Bearer 业务 key 返回用量统计,对齐 OpenAI 返回格式

2.5 ✅ 阶段 2 回归

  • /admin 301 跳 /dashboard;/dashboard 各页面与静态资源正常加载(无 404、无样式丢失)
  • 面板内音色增删改、全局设置、CORS 配置全部正常
  • 旧路径 /api/voices、/api/settings 直接可用(不是重定向),行为与新路径一致
  • GET /api/user/usage、GET /v1/dashboard/billing/usage 正常返回
  • 用业务 Bearer key 访问 /api/admin/* 返回 401
  • 完整安装流程跑通,装完跳转 /dashboard

阶段 3|文档、配置、发布

改动范围:README.md、.env.example、docker-compose.yml、CHANGELOG.md

  1. README.md
    • WebUI 入口改为 /dashboard;注明 /admin 为旧兼容重定向
    • 端点表更新:/metrics、/health 默认 404;新增 /healthz、METRICS_ALLOW_CIDR���admin_key 说明
    • 公网部署安全清单更新(整节重写,明确"哪些端点不鉴权、必须靠什么挡")
    • 探针迁移提醒:K8s / Docker HEALTHCHECK 改指 /healthz
  2. .env.example:新增 METRICS_ALLOW_CIDR 注释(含 Docker 网段警告)
  3. docker-compose.yml:METRICS_ALLOW_CIDR 注释示例
  4. CHANGELOG.md:v0.3.0 条目,明确列出 Breaking Change

Release Note · Breaking Change 清单

  1. /metrics、/health、/dashboard 不再匿名可读。Prometheus 抓取请用 /api/admin/metrics(带 Bearer),或配置 METRICS_ALLOW_CIDR; 探针请改用 /healthz。
  2. 未配置任何凭证时服务拒绝启动(此前管理接口完全裸奔)。
  3. 管理凭证与业务凭证可分离:新增 admin_key;配了之后业务 key 无法访问管理接口。
  4. 管理接口迁移到 /api/admin/*;旧路径保留为别名(非重定向),未来版本移除。
  5. 管理入口迁移到 /dashboard;/admin 301 永久重定向。

阶段 4|全量回归测试(发布前必须全部通过)

⚠️ 本项目测试文件不入库(.gitignore 的 *_test.go),CI 只能守住"编译 + vet", 守不住行为回归。因此下面的手工回归是发布前的唯一防线,必须逐条执行并记录结果。

✨ 安装流程

  1. 不设 TTS_ADMIN_KEY 启动 → 终端打印一次性密钥(带醒目警告)
  2. 不带密钥 POST /api/setup → 401;带密钥 → 安装成功
  3. 安装完成跳转 /dashboard;进程重启后旧一次性密钥失效
  4. 已安装状态访问 /setup → 自动跳 /dashboard

✨ 管理面板

  1. 未登录访问 /dashboard → 显示登录视图;登录后完整可用
  2. 访问 /admin → 301 跳 /dashboard
  3. 音色 CRUD、全局设置、CORS 修改全部正常
  4. 仪表盘通过 /api/admin/health、/api/admin/metrics 渲染正常
  5. 用量面板读取 /api/user/usage 正常

✨ 指标与健康端点

  1. 不配 METRICS_ALLOW_CIDR → 根 /metrics、/health 返回 404
  2. 配 METRICS_ALLOW_CIDR=127.0.0.1/32 → 本机根路径可访问,其他 IP 404
  3. /healthz 匿名可访问且响应体不含任何业务字段

✨ 业务接口

  1. /v1/audio/speech OpenAI TTS 合成完全正常
  2. GET /v1/dashboard/billing/usage 合法 key → 200;非法 key → 401
  3. 业务 Bearer key 访问 /api/admin/* → 401(隔离生效)

✨ 存量兼容

  1. 旧管理接口路径/api/voices、/api/settings 直接可用,行为与新路径一致

✨ 安全场景

  1. 未配置凭证时服务拒绝启动
  2. 匿名无法获取任何健康 / 指标数据
  3. 旧 tts.db 直接复用升级,无需数据库迁移

✨ 本地自动化

  1. go build ./... && go vet ./... 通过
  2. go test ./... -count=1 全绿 (含抓出过 VoiceList 语义 bug 的那批集成测试,是本次改造最重要的安全网)

3. 发布 v0.3.0

  1. 阶段 4 全部通过;Docker 镜像构建测试通过
  2. git tag v0.3.0 并推送
  3. 创建 Release,粘贴 Release Notes,明确列出 5 条 Breaking Change
  4. 创建维护分支 v0.3.x,用于后续 bugfix 与安全补丁
  5. develop 继续迭代 v0.4.0;上游适配器重构可并行

4. v0.4.0 后续规划(仅规划,不在 v0.3.0 实现)

v0.3.0 已把路由分层和权限隔离打好;v0.4.0 只需填充业务逻辑。

  1. store 新增 users 表,区分普通用户 / 管理员角色
  2. 引入真正的会话机制(此时才需要 Cookie + CSRF,或采用 Bearer-per-user)
  3. /api/user/* 从桩转真实业务:个人用量、个人 API 密钥管理、个人调用日志
  4. /api/admin/* 实现用户 CRUD、配额管理;音色归属绑定用户
  5. SPA 扩展个人中心、密钥管理页面
  6. /v1/dashboard/billing/usage 按 API-Key 归属过滤统计

5. 风险注意事项

# 风险 对策
1 /healthz 与 /health 加鉴权分批上线 → 探针空窗、Pod 重启循环 必须同一批改动内落地
2 未配凭证导致服务启动失败(阶段 1 的破坏性结果) 升级前先配 auth_key;README 与 Release Note 明确提示
3 Docker 内 METRICS_ALLOW_CIDR 填 127.0.0.1/32 必须填容器内网网段
4 禁止把 RequireAdmin 套到 /setup 安装阶段无数据库、无凭证,保留 InstallGuard
5 旧路径若用重定向会导致 POST 降级 / 客户端无感 采用别名,不重定向
6 静态资源路由遗漏导致面板丢样式 迁移时同步 /dashboard/admin.css、/dashboard/admin-shell.js
7 CI 跑不到测试(测试文件不入库) 每阶段结束本地执行 go test ./... -count=1
8 误改 adapter/、store/ 业务模型 本改造不涉及;改动仅限 router / middleware / controller / 前端 / 文档

附录 A|与初版方案的差异

初版方案 本版 原因
阶段 0 "重命名 AdminSessionMiddleware" 删除该阶段 该中间件不存在;实为新建 Cookie 会话层,会把"低风险重构"变成破坏性变更
新增 UserSessionMiddleware 不做 没有会话机制;/dashboard 页面请求不带 sessionStorage,服务端无从判断登录态
新增 CSRFMiddleware 不做 Bearer 认证免疫 CSRF(浏览器不会自动附带 Authorization);CORS 已在中间件层 403 非白名单 Origin
Cookie HttpOnly / SameSite=Strict 不做 不使用 Cookie
promhttp.Handler() metrics.Meter.Handler() 项目未引入 prometheus/client_golang
controller.AdminSPAPageHandler / AdminLoginPageHandler serveAdmin(adminHTML) 等 这两个 handler 不存在
漏点只列 /metrics、/health 补入 /dashboard、/api/setup/status /dashboard 聚合度最高;实测确认这两个也匿名可达
旧 API 路径 301 重定向 别名,不重定向 301 会把 POST 降级为 GET 且被永久缓存
未提静态资源路由 明确 /dashboard/admin.css 等 遗漏会导致面板丢样式
RequireAdmin 空凭证放行未处理 阶段 1 专门修复 + fail-fast 不修则后续所有鉴权改动形同虚设

初版保留不变的部分

分阶段 + 每阶段可编译可回归的原则、InstallGuard 保持不动、/v1/dashboard/billing/usage 走 Bearer、/api/public 与 /api/user 留桩、Docker CIDR 网段警告、v0.4.0 规划、发布流程。


  1. 不解决问题:目标是"健康数据不外泄",用现有 RequireAdmin 即可 100% 达成, 不需要会话层。
  2. 服务端拿不到 SPA 登录态:前端凭证存 sessionStorage,不随请求发送, 所以服务端无法判断 /dashboard 页面请求是否已登录。初版要的"未登录访问 /dashboard 自动跳登录页"在服务端做不到——除非引入 Cookie,而这正是初版的前提缺失之处。
  3. CSRF 需求随之消失:Cookie 才需要 CSRF 防护;Bearer + sessionStorage 天然免疫。 不做 Cookie 就不必实现 token 发放与轮换,也避开同源 SPA 下 SameSite=Strict 的坑。
  4. 破坏现有客户端:现有用 Authorization: Bearer 调管理 API 的脚本会全部 401, 与"给外部脚本过渡期"的目标冲突。
  5. 真正的权限问题另有其解:当前"一个 key 既是管理密码又是业务密钥"才是实质缺陷, 用新增可选 admin_key 即可分离,成本约 20 行,无需引入会话。

文档版本:v2 · 校正基线:develop @ 49f57e4(2026-10-04) 初版:v1(外部产出)· 校正原因见附录 A