feat(auth): v0.3.0 阶段1 收紧管理鉴权并分离管理/业务凭证
修一个前置缺口:RequireAdmin 此前在凭证列表为空时直接放行(len(keys)==0 -> next.ServeHTTP),导致未配置凭证的部署上管理接口完全裸奔,并且会让后续给 /metrics、/health 套该中间件的加固形同虚设。现改为拒绝(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 /api/settings 仅回打码值与来源)。 normal 模式下管理凭证为空时启动期 fail-fast:RequireAdmin 改为拒绝后,若此处不拦,服务会正常起来但 /dashboard 与全部 /api/admin/* 都是 401,等于把自己锁在门外。启动摘要同时打印管理凭证来源(admin_key / auth_key / 未配置)。 验证:新增 middleware/admin_auth_test.go 覆盖空凭证拒绝、正确/错误/缺前缀/空白 token、OPTIONS 放行,以及权限隔离的两个场景(配了 admin_key 时业务 key 401;回退模式下业务 key 200)。go build / go vet / go test ./... -count=1 全绿。
This commit is contained in:
@@ -32,6 +32,14 @@ var (
|
||||
ttsTimeout time.Duration = common.DefaultTimeout
|
||||
ttsConfigErr error
|
||||
authAPIKeys []string
|
||||
// adminAPIKeys 是**管理接口专用**凭证(admin_key)。
|
||||
// 与 authAPIKeys(业务侧,/v1/audio/speech 用)分离:
|
||||
// - admin_key 为空时回退到 auth_key/env,老部署行为完全不变
|
||||
// - admin_key 配了之后,业务 key 无法访问 /api/admin/*,权限隔离成立
|
||||
// 详见 docs/IMPLEMENT_v0.3.0.md 阶段 1。
|
||||
adminAPIKeys []string
|
||||
// adminKeySource 记录管理凭证的来源,仅用于启动摘要与排障。
|
||||
adminKeySource string
|
||||
// corsAllowAll / corsOrigins 拆成两个独立字段,各自在 RLock 下读取,
|
||||
// 避免 CORSConfig 整体读时被 Lock 阻塞热路径。
|
||||
corsAllowAll bool
|
||||
@@ -108,6 +116,40 @@ func SetAuthAPIKeys(keys []string) {
|
||||
authAPIKeys = out
|
||||
}
|
||||
|
||||
// GetAdminKeys 读**管理接口专用**凭证列表;返回拷贝防止业务侧持有底层 slice。
|
||||
// 供 middleware.RequireAdmin 使用;业务侧鉴权请用 GetAuthAPIKeys。
|
||||
func GetAdminKeys() []string {
|
||||
ttsMu.RLock()
|
||||
defer ttsMu.RUnlock()
|
||||
if len(adminAPIKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(adminAPIKeys))
|
||||
copy(out, adminAPIKeys)
|
||||
return out
|
||||
}
|
||||
|
||||
// SetAdminKeys 整体替换管理凭证;入参被复制。source 仅用于启动摘要展示。
|
||||
func SetAdminKeys(keys []string, source string) {
|
||||
ttsMu.Lock()
|
||||
defer ttsMu.Unlock()
|
||||
adminKeySource = source
|
||||
if len(keys) == 0 {
|
||||
adminAPIKeys = nil
|
||||
return
|
||||
}
|
||||
out := make([]string, len(keys))
|
||||
copy(out, keys)
|
||||
adminAPIKeys = out
|
||||
}
|
||||
|
||||
// GetAdminKeySource 返回管理凭证来源:admin_key / auth_key / env / ""(未配置)。
|
||||
func GetAdminKeySource() string {
|
||||
ttsMu.RLock()
|
||||
defer ttsMu.RUnlock()
|
||||
return adminKeySource
|
||||
}
|
||||
|
||||
// GetCORSAllowAll 读 CORS 是否放行所有来源。
|
||||
func GetCORSAllowAll() bool {
|
||||
ttsMu.RLock()
|
||||
@@ -391,6 +433,18 @@ func LoadRuntimeConfig(s Store) error {
|
||||
SetAuthAPIKeys(nil)
|
||||
}
|
||||
|
||||
// 管理凭证:admin_key(DB) > auth_key(DB) > OPENAI_TTS_API_KEY(env)
|
||||
// 分离的目的:业务调用方拿到的 key 不应同时拥有管理后台权限。
|
||||
// 不配 admin_key 时行为与旧版完全一致(回退用 auth_key),保证向后兼容。
|
||||
switch {
|
||||
case all["admin_key"] != "":
|
||||
SetAdminKeys([]string{all["admin_key"]}, "admin_key")
|
||||
case authKey != "":
|
||||
SetAdminKeys([]string{authKey}, "auth_key")
|
||||
default:
|
||||
SetAdminKeys(nil, "")
|
||||
}
|
||||
|
||||
// CORS 配置:DB > env
|
||||
// cors_allow_all (bool): 允许所有来源(*)
|
||||
// cors_origins (string): 逗号分隔白名单
|
||||
@@ -540,6 +594,16 @@ func LogStartupSummary() {
|
||||
log.Printf("OPENAI_TTS_API_KEY: 已设置 %d 个有效密钥", len(authKeys))
|
||||
}
|
||||
|
||||
// v0.3.0:管理凭证独立于业务凭证(admin_key > auth_key > env)
|
||||
switch GetAdminKeySource() {
|
||||
case "admin_key":
|
||||
log.Printf("管理凭证: 使用独立 admin_key(业务 key 无法访问管理接口)")
|
||||
case "auth_key":
|
||||
log.Printf("管理凭证: 未单独配置 admin_key,回退使用 auth_key(业务 key 同时拥有管理权限)")
|
||||
default:
|
||||
log.Printf("管理凭证: ✗ 未配置(normal 模式下服务将拒绝启动)")
|
||||
}
|
||||
|
||||
allowAll := GetCORSAllowAll()
|
||||
origins := GetCORSOrigins()
|
||||
if allowAll {
|
||||
|
||||
Reference in New Issue
Block a user