From c78a473e59ca76c063901061b459ff48fbc927db Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:34:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20API=20Key=20=E5=89=8D=E7=BC=80=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20sk-ot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apikey 包 prefix 常量 sk- → sk-ot(生成/校验/展示前缀统一驱动) - 鉴权错误提示同步更新为 sk-ot 格式 - 测试断言改用常量,避免硬编码长度 - 前端 curl 示例与 README/PLANNING 文档同步 --- PLANNING.md | 4 ++-- README.md | 2 +- server/internal/pkg/apikey/apikey.go | 6 +++--- server/internal/pkg/apikey/apikey_test.go | 17 ++++++++++------- server/internal/proxy/gateway.go | 2 +- web/src/views/LandingView.vue | 4 ++-- web/src/views/console/DashboardView.vue | 2 +- 7 files changed, 20 insertions(+), 17 deletions(-) diff --git a/PLANNING.md b/PLANNING.md index 325ad52..33cabbb 100644 --- a/PLANNING.md +++ b/PLANNING.md @@ -275,8 +275,8 @@ Anthropic /v1/messages ──┘ #### 5.3.3 API Key -- 格式:`sk-` + 48 位随机 base62,**创建时仅展示一次**。 -- 存储:仅 SHA-256 哈希 + 展示前缀(如 `sk-aB3c…`);请求时哈希后查表。 +- 格式:`sk-ot` + 48 位随机 base62,**创建时仅展示一次**。 +- 存储:仅 SHA-256 哈希 + 展示前缀(如 `sk-otaB3c…`);请求时哈希后查表。 - 附加能力:密钥级配额(每日 token / 请求数)、模型白名单、过期时间、启停。 - 限额检查用 Redis 计数,与用户级限流叠加。 diff --git a/README.md b/README.md index c8fb8d7..62f6bf6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - **限流/配额**(内存计数):密钥级每日请求数 / Token 数配额、用户级每秒速率(`OT_RATELIMIT_USER_RPS`);超限返回 429 - **前端**:Dashboard / 管理总览骨架屏加载态 - **用户体系**:注册(开放/邀请码可切换,管理后台可改)、登录(JWT access + HttpOnly refresh cookie)、argon2id 密码 -- **API Key**:`sk-` 48 位 base62,仅存 SHA-256 哈希,明文一次性展示;支持限额/过期/白名单字段 +- **API Key**:`sk-ot` 48 位 base62,仅存 SHA-256 哈希,明文一次性展示;支持限额/过期/白名单字段 - **用量计费**:请求级 `usage_logs` 异步批量落库,按模型价格扣减余额,日粒度预聚合(`usage_daily`) - **管理 API**:渠道 CRUD + 连通测试 + 模型导入、模型管理 + 定价 + 渠道绑定、用户管理、全局用量/统计、系统配置 - **前端**(Vue3 + Tailwind,taste-skill 设计,深色优先) diff --git a/server/internal/pkg/apikey/apikey.go b/server/internal/pkg/apikey/apikey.go index 1420d62..114b56b 100644 --- a/server/internal/pkg/apikey/apikey.go +++ b/server/internal/pkg/apikey/apikey.go @@ -1,4 +1,4 @@ -// Package apikey 生成与管理 API Key:sk- + 48 位 base62 随机串。 +// Package apikey 生成与管理 API Key:sk-ot + 48 位 base62 随机串。 // 库中仅存 SHA-256 哈希与展示前缀(PLANNING §4.3.3)。 package apikey @@ -12,7 +12,7 @@ import ( const ( alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" keyLen = 48 - prefix = "sk-" + prefix = "sk-ot" ) // Generate 生成明文 key(仅创建时展示一次)与哈希、前缀。 @@ -34,7 +34,7 @@ func Hash(key string) string { return hex.EncodeToString(sum[:]) } -// Prefix 展示前缀:sk-aB3cD5…(前 12 字符) +// Prefix 展示前缀:sk-ot…(前 12 字符) func Prefix(key string) string { if len(key) <= 12 { return key diff --git a/server/internal/pkg/apikey/apikey_test.go b/server/internal/pkg/apikey/apikey_test.go index 982d9e3..4951766 100644 --- a/server/internal/pkg/apikey/apikey_test.go +++ b/server/internal/pkg/apikey/apikey_test.go @@ -3,32 +3,35 @@ package apikey import "testing" func TestGenerateValid(t *testing.T) { - plain, hash, prefix, err := Generate() + plain, hash, keyPrefix, err := Generate() if err != nil { t.Fatalf("Generate: %v", err) } if !Valid(plain) { t.Fatalf("generated key invalid: %q", plain) } - if len(plain) != len("sk-")+48 { + if len(plain) != len(prefix)+keyLen { t.Fatalf("unexpected key length: %d", len(plain)) } + if keyPrefix != plain[:12] { + t.Fatalf("prefix mismatch: %s vs %s", keyPrefix, plain[:12]) + } if Hash(plain) != hash { t.Fatal("hash mismatch") } - if prefix != plain[:12] { - t.Fatalf("prefix mismatch: %s vs %s", prefix, plain[:12]) - } } func TestHashStable(t *testing.T) { - if Hash("sk-test") != Hash("sk-test") { + if Hash("sk-ot-test") != Hash("sk-ot-test") { t.Fatal("hash not stable") } } func TestValidRejects(t *testing.T) { - cases := []string{"", "sk-abc", "abc-123456789012345678901234567890123456789012345678", "sk-1234567890123456789012345678901234567890123456789"} + tooShort := prefix + "abc" + tooLong := prefix + "1234567890123456789012345678901234567890123456789" + wrongPrefix := "abc-ot123456789012345678901234567890123456789012345678" + cases := []string{"", tooShort, wrongPrefix, tooLong} for _, c := range cases { if Valid(c) { t.Fatalf("expected invalid: %q", c) diff --git a/server/internal/proxy/gateway.go b/server/internal/proxy/gateway.go index 3035f40..cf1b114 100644 --- a/server/internal/proxy/gateway.go +++ b/server/internal/proxy/gateway.go @@ -142,7 +142,7 @@ func (g *Gateway) Auth(c *gin.Context) { key = strings.TrimSpace(c.GetHeader("x-api-key")) } if !apikey.Valid(key) { - apiError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key format. Expected: Authorization: Bearer sk-... or x-api-key: sk-...") + apiError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key format. Expected: Authorization: Bearer sk-ot... or x-api-key: sk-ot...") c.Abort() return } diff --git a/web/src/views/LandingView.vue b/web/src/views/LandingView.vue index 38961cd..37192b1 100644 --- a/web/src/views/LandingView.vue +++ b/web/src/views/LandingView.vue @@ -79,7 +79,7 @@ const baseURL = `${window.location.origin}/v1`

$ curl {{ baseURL }}/chat/completions

-

-H "Authorization: Bearer sk-..." \

+

-H "Authorization: Bearer sk-ot..." \

-d '{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "你好"}]}'

@@ -136,7 +136,7 @@ const baseURL = `${window.location.origin}/v1`

-

sk-…

+

sk-ot…

API Key 管理

密钥仅存哈希,支持配额、过期与模型白名单,创建时一次性展示。 diff --git a/web/src/views/console/DashboardView.vue b/web/src/views/console/DashboardView.vue index 41e7cc0..6142b74 100644 --- a/web/src/views/console/DashboardView.vue +++ b/web/src/views/console/DashboardView.vue @@ -49,7 +49,7 @@ function daysAgo(n: number): string { // 快速开始:Base URL 用浏览器当前 host(兼容端点),curl 示例 const baseURL = `${window.location.origin}/v1` const curlCmd = `curl ${baseURL}/chat/completions \\ - -H "Authorization: Bearer sk-xxxx" \\ + -H "Authorization: Bearer sk-ot-xxxx" \\ -H "Content-Type: application/json" \\ -d '{"model":"gpt-test","stream":true,"messages":[{"role":"user","content":"hi"}]}'` const copied = ref('')