sun
|
1e2e37ae97
|
feat(auth)!: v0.2.4 阶段一 鉴权收紧 + 健康/指标端点收口
本提交由初版方案的「阶段 1」与「阶段 2」合并而成。合并原因:两者是同一件事的两半——
只收紧鉴权而不收口端点,结果是"有风险无收益"(未配凭证的旧部署可能起不来,
而 /health、/dashboard、/metrics 仍匿名可读)。
一、鉴权收紧(原阶段 1)
- RequireAdmin 在凭证列表为空时不再放行。此前 len(keys)==0 直接 next.ServeHTTP,
导致未配凭证的部署上管理接口完全裸奔,也让后续给端点加鉴权的加固形同虚设。
现在该情况返回 401 并记录明确日志。
- 新增可选独立管理凭证 admin_key,取值优先级:
admin_key(DB) > auth_key(DB) > OPENAI_TTS_API_KEY(env)。
middleware.ValidateAPIKey(业务侧 /v1/audio/speech)保持只看 auth_key,
于是配置 admin_key 后业务调用方持有的 key 无法访问管理接口,权限隔离成立;
不配置则回退 auth_key,老部署行为不变。
新增 PUT /api/admin/settings/admin-key 与旧前缀别名,
凭证只写不读(GET 仅返回打码值与 admin_key_set / admin_key_source)。
- normal 模式下管理凭证为空时启动期 fail-fast。RequireAdmin 改为拒绝后,
若此处不拦,服务会正常起来但 /dashboard 与全部 /api/admin/* 都是 401,
等于把自己锁在门外;宁可启动失败并打印明确原因。
二、端点收口(原阶段 2)
- /health 改为鉴权(原匿名泄漏版本、commit、运行时长、内存、goroutine、配置错误文本)
- /dashboard 改为鉴权,并对浏览器 HTML 请求做内容协商(Accept 含 text/html 时
返回页面外壳,由前端据 sessionStorage 显示登录视图;非 HTML 请求无凭证一律 401)。
不做无条件 401 的原因:SPA 登录态存在 sessionStorage、不随请求发送,
服务端无从判断是否已登录,强行 401 会把"打开看到登录页"变成"打开直接报错"。
页面外壳不含任何数据,数据全部来自鉴权后的 API。
- /api/setup/status 改为鉴权(原本 normal 模式下仍匿名返回 installed/mode)
- 根路径 /metrics 默认完全不注册(访问 404);配置 METRICS_ALLOW_CIDR 后按内网
白名单开放,非白名单返回 404 而非 403,不向扫描者确认端点存在。
- 新增 GET /healthz 匿名存活探针,只回 200 与字面量 ok、不含任何字段,解决
"给 /health 加鉴权后 K8s 探针与 Docker HEALTHCHECK 会一律 401 导致 Pod 反复重启";
新增 GET /api/admin/health 鉴权版详细健康数据。
IP 白名单中间件复用 GetClientIP(已处理 XFF 与 TRUSTED_PROXY_HOPS);
main.go 启动日志同步改为指向 /healthz 与 /api/admin/health。
BREAKING CHANGE:
1. /health、/dashboard、/api/setup/status 不再匿名可读;根路径 /metrics 需配
METRICS_ALLOW_CIDR,否则 404。
2. 未配置 auth_key / admin_key / OPENAI_TTS_API_KEY 任一时,normal 模式下服务拒绝启动。
3. K8s 探针与 Docker HEALTHCHECK 必须改指 /healthz;Prometheus 请改用鉴权版
/api/admin/metrics 或配置 METRICS_ALLOW_CIDR(Docker 中勿填 127.0.0.1/32,
那是容器自身回环,应填容器内网网段)。
验证:go build / go vet / go test ./... -count=1 全绿(7 个包);
另用真实服务器端到端验证端点矩阵、内容协商、凭证隔离与 CIDR 白名单两种形态。
|
2026-10-04 01:17:06 +08:00 |
|
sun
|
19d83f7a3b
|
fix(setting): runtime config 全局变量加 sync.RWMutex 保护,避免半写状态
TTSOptions / TTSTimeout / TTSConfigErr / Auth.APIKeys / CORS 在运行期会被
LoadRuntimeConfig(PUT /api/settings 触发)整体替换,struct 整体赋值不是原子的,
若 TTS 请求正并发读,可能拿到半写状态。单用户场景概率低,属正确性隐患。
改动:
- setting/config.go: 把可变运行时配置迁到包内私有变量,新增 ttsMu
(sync.RWMutex) 统一保护,提供 Get*/Set* 访问器
- GetTTSOptions/SetTTSOptions: 值类型快照,读端无锁开销
- GetTTSTimeout/SetTTSTimeout
- GetTTSConfigErr/SetTTSConfigErr
- GetAuthAPIKeys/SetAuthAPIKeys: 拷贝进出,防止外部持有底层 slice
- GetCORSAllowAll/GetCORSOrigins/SetCORS: AllowAll 和 Origins 拆开,
避免热路径 CORSConfig 整体读时锁粒度过粗
- 删除导出可变全局 var TTSOptions/TTSTimeout/TTSConfigErr/Auth/CORS,
编译期强制所有读写走 Get*/Set*
- Server.Port/TrustedProxyHops/SetupToken 仅启动期写,运行期无并发修改,
保持原状不加锁
- LoadRuntimeConfig/InitAuthConfig/InitCORSConfig 全切到 Set*
- CheckEnvironmentVariables/LogStartupSummary 一次性拿快照,
缩短锁占用窗口,避免多次 log.Printf 之间数据被替换
调用点:
- controller/tts.go: 热路径 opts := setting.GetTTSOptions(),
context 超时走 GetTTSTimeout(),/health 块用 GetTTSConfigErr
- controller/settings.go: 写改 SetAuthAPIKeys/SetCORS
- controller/setup.go: 写改 SetAuthAPIKeys
- middleware/auth.go: 鉴权改 GetAuthAPIKeys()(在 RLock 下拿拷贝)
- middleware/admin_auth.go: 同上
- middleware/cors.go: matchOrigin 改 GetCORSAllowAll/GetCORSOrigins
读多写少,RWMutex 读路径不互斥,不会拖慢热路径。
|
2026-09-21 10:50:03 +08:00 |
|
sun
|
9353790899
|
refactor: 删 trimAll + 修 isSensitive,统一用 stdlib
两处自实现 / 半成品:
1. controller/settings.go trimAll (11 行)
- 自己写循环 trim 4 个 ASCII 字符 (' ' \t \n \r)
- 等价于 strings.TrimSpace (后者 trim 所有 Unicode 空白)
- strings 包已 import (其他代码用着), 没引用代价
- 改: 7 个调用点全替, 删函数定义
2. cmd/dumpdb/main.go isSensitive (13 行)
- 自实现 strings.Contains 循环, 没调 strings.ToLower
- 变量名叫 lowK 但实际从未小写化
- Bug: API_KEY / Auth_Token 这类大写 key 不会被判为敏感, 漏打码
- 修: 用 strings.ToLower + strings.Contains, 大小写不敏感
- 顺手加 strings import
收益:
- 行数: +15 / -27 (净删 12 行)
- 一致性: 看到 strings.TrimSpace / strings.Contains 就懂, 不必读自实现
- 安全: 大写 key 之前漏打码, 修了
e2e 验证 (verifydbsens 临时脚本, 已删):
api_key -> sensitive ✓
auth_key -> sensitive ✓
API_KEY_UPPER -> sensitive ✓ (修前漏)
Auth_Token_Mixed -> sensitive ✓ (修前漏)
default_speaker -> sensitive ✓
default_resource_id -> not sensitive ✓
note -> not sensitive ✓
build + go test ./... 全绿
未 push (待用户)
|
2026-09-05 11:59:06 +08:00 |
|
sun
|
59c0027122
|
fix(cors): SettingsCORSRequest.Origins 改 *string,支持显式清空
Bug 2 报告: controller/settings.go:330-332 的死代码
if body.AllowAll == nil && trimAll(body.Origins) == "" && body.Origins != "" {
// 空 body 不算错误
}
该条件永远 false (trim 空 != "" 矛盾), 整个 if 块只注释不执行。
更严重的是: 用户无法清空 origins —
原代码 body.Origins == "" 就 400, 但用户可能就是想传空串清空。
struct 字段是 string, 区分不出"未传"和"传空串"。
修法:
- struct.SettingsCORSRequest.Origins: string → *string
- nil = 未传 (不动)
- *"" = 传空串 (清空)
- *"url1\nurl2" = 覆盖
- 删掉 line 330-332 死代码
- 改校验逻辑:
- 2 个字段都 nil → 400 no_fields
- 至少一个非 nil → 200
- 空串不校验 origin 格式 (无 origin 可校), 直接存
行为变更:
- {"origins": "https://a.com"} → 写, 同以前
- {"origins": ""} → 清空 (新功能, 以前返 400)
- {"origins": null} → 视为未传, 不动
- {"allow_all": true} → 写, 同以前
- {"allow_all": false} → 设为 false, 同以前
- {} → 400 no_fields
- {"origins": "not-a-url"} → 400 origin_invalid (同以前)
测试: test/integration/settings_test.go (gitignored) 5 个 case
- EmptyOrigins_ClearsOrigins
- OriginsNotInBody_NotTouched
- EmptyBody_400
- NullOrigins_TreatedAsMissing
- InvalidOrigin_400
e2e: 4 场景全过 (设/清空/空 body 400/null 不动)。
不向后兼容: SettingsCORSRequest 内部 DTO, 仅 admin webui 调,
不暴露给外部 OpenAI 客户端。
未 push (待用户)
|
2026-09-05 11:42:48 +08:00 |
|
sun
|
7660db14d8
|
fix(cors): CORS 全 DB 化,install 模式完全跳过,WebUI 可配
完整方案: 与 auth_key 同思路,让 CORS 也走 DB,彻底干掉 ALLOWED_ORIGINS env。
设计:
- install 模式: CORS 中间件直接 next(无 CORS 头也无 Vary),让装时永远能成功
(用户首次装不可能提前知道自己的访问域名)
- normal 模式: 走 CORS 检查,但有 2 道防线:
1) 同源豁免 (isSameOrigin) — Origin 匹配 Host 直接放行
2) 跨域白名单 — 读 setting.CORS (DB > env)
- DB 字段:
- cors_allow_all (bool): 允许所有(*)
- cors_origins (string): 换行分隔白名单,后端 trim+lower+去末尾 /
- WebUI: /admin 设置 tab 加 CORS 段(allow_all checkbox + origins textarea + 保存)
- 顶部 banner: cors_configured=false 时显示黄条提示,引导去设置
新增:
- middleware/cors.go: 顶部 installer.GetMode() == ModeSetup 时直接 next
- setting/config.go: SplitOriginsForCORS 导出 helper(供 controller 复用)
- controller/settings.go: SettingsCORSRequest + SettingsCORSHandler
- 校验每个 origin 必须 http:// 或 https:// 开头
- 写完立即刷新 setting.CORS.AllowAll + setting.CORS.Origins(下个跨域请求生效)
- 拒绝空 body
- router/router.go: 挂 /api/settings/cors (PUT, RequireAdmin)
- router/admin.html: 设置 tab 加 CORS card + 顶部 banner (31.19KB, 略超 30KB 预算可接受)
改造:
- setting/config.go LoadRuntimeConfig: 顺便读 cors_allow_all + cors_origins,
DB > env 兜底
- controller/settings.go SettingsResponse: 加 cors_allow_all + cors_origins + cors_configured
e2e 跑通(本机)10 场景:
- install 任意 Origin → 200 (install mode 跳 CORS)
- normal 同源 → 200
- normal 跨域无 allowlist → 403
- GET settings cors_configured=false
- PUT /api/settings/cors → 200
- GET settings cors_configured=true
- 跨域命中白名单 → 200
- 跨域未命中白名单 → 403
- allow_all=true → 任意跨域 200
- 重启后 CORS 仍从 DB 加载
未 push(等用户测试 install 是否顺利,以及 CORS banner 是否合适)
|
2026-08-30 00:08:14 +08:00 |
|
sun
|
a4ea53bfa0
|
fix(auth): OpenAI 端 key 也走 DB,setup 是单一配置入口
修 M2/M3 遗漏:OPENAI_TTS_API_KEY (admin + 合成鉴权) 此前只能从 env
读,setup 前端没收集。装完用户还要回去设 env,体验断,违反
'setup 是单一配置源' 承诺。
改动:
- setting/config.go: LoadRuntimeConfig 顺便把 auth_key 灌到
setting.Auth.APIKeys。优先级 DB > env(env 仅作 fallback,首次启
动无 DB 时仍可用,向后兼容)。
- controller/setup.go: validateSetupSettings 加 auth_key 为必填,
写 DB 后立即 setting.Auth.APIKeys = [auth_key](本进程内立刻生效)。
- controller/settings.go: GET /api/settings 返 auth_key (打码) +
auth_key_set;新增 SettingsAuthKeyHandler (PUT /api/settings/auth-key),
改完单独刷新 setting.Auth.APIKeys(避免被自踢,只下次请求生效)。
- router/router.go: 挂 /api/settings/auth-key 路由。
- router/setup.html: 加 OpenAI 鉴权 Key 字段(密码框,带提示);
表单 default + submit 都加 auth_key。
- router/admin.html: 设置 tab 拆两张卡 — 鉴权 Key(独立保存按钮,
改完会自踢当前会话)+ 火山 TTS 凭证;vue setup 加 authKeyInput +
saveAuthKey 函数。
- main.go: 把 LoadRuntimeConfig(st) 提到 LogStartupSummary 之前,
让启动日志反映真实运行时状态(DB 已加载的 key 数量)而不是
过时的 env 状态。
未 push(等用户当前 M3 测完一起或单独)
|
2026-08-29 23:18:51 +08:00 |
|
sun
|
9db038dd05
|
feat(settings): M3 全局设置 + voice 动态路由 + env 收敛
M3 完成:env 业务配置全进 DB,WebUI 可改,改完立即生效(无需重启)。
新增:
- controller/settings.go: 3 个 handler
- GET /api/settings: 读所有设置,api_key 永远打码
- PUT /api/settings: 部分更新(白名单字段);default_speaker 改时校验
音色必须在 voices 表里(避免引用不存在);改完调 LoadRuntimeConfig 即时生效
- PUT /api/settings/api-key: 独立端点,只改 api_key
改造:
- setting/config.go: 用 LoadRuntimeConfig(store) 替代 InitTTSConfig
- 不再读 BYTEDANCE_TTS_* env,完全从 store.Settings 拿
- 必填项缺失返 error,设 TTSConfigErr(/v1/audio/speech 返 503)
- 定义 Store 接口(避免 setting → store 循环 import)
- 字段映射: BYTEDANCE_TTS_API_KEY → api_key, BYTEDANCE_TTS_RESOURCE_ID
→ default_resource_id, ... 全部一一对应
- controller/tts.go: voice 路由
- voice 为空 → 用 setting.TTSOptions.Speaker (即 store.default_speaker)
- voice 非空 → 查 voices 表,替换 opts.Speaker / ResourceID / Model
- 命中但 enabled=0 → 仍可用(显式 voice 覆盖)
- 未命中 → 400 unknown_voice
- voice 路由日志: "[tts] voice=xxx 命中 (speaker=... resource=... model=...)"
- router/admin.html: 加 #settings tab (24.78KB, 仍 <30KB 预算)
- API key 区:打码显示 + 独立更新按钮
- 路由/格式区:default_resource_id / default_speaker (下拉源 /api/voices) /
default_format / sample_rate / model
- "保存" 按钮 PUT /api/settings + 立即 reload (settingsOk 提示)
- router/router.go: 挂 /api/settings[/{api-key}] 三路由
- main.go: Detect 之后调 LoadRuntimeConfig,失败也允许进入
(env 阶段无 db,LogStartupSummary 仍显示 BYTEDANCE_TTS_* 必填项用于诊断)
测试(都被 .gitignore 排除,本地保留):
- 已有 store/installer/middleware/router/telemetry 测试保持全绿
- e2e 验证(本机跑通 9 场景):
- install 写入 3 voices + 7 settings
- 重启后 main.go log: "TTS 运行时配置已加载(api_key=***, speaker=default-voice, resource=volc.megatts.icl, format=wav)" — 确认走 DB 不读 env
- GET /api/settings 返所有字段
- voice=clone-voice-1 命中 S_clone_1 + seed-tts-2.0-expressive
- voice=clone-voice-2 命中 S_clone_2 + volc.megatts.icl-2
- voice=unknown 返 400 unknown_voice
- PUT /api/settings 改 default_speaker 即时生效
- PUT /api/settings/api-key 改 key 即时生效
- PUT /api/settings default_speaker=ghost 返 400 default_speaker_missing
注意: e2e 里的 TTS 500 是因为我用假 key 调上游,路由逻辑本身正确。
换真火山 API key 即可 work。
二进制大小: ~17MB → ~18MB (+1MB)
未 push(等用户测试 M1+M2+speaker fix 全部稳定后,跟 51bd7ae+70865a3 一起或单独 push)
|
2026-08-29 22:40:53 +08:00 |
|