Files
22ee0aa112 修复安全问题 (#9)
* fix:修复权限问题

* fix:修复项目key类型错误问题

* fix:修复rbac权限问题

* fix:修复api密钥未及时失效的问题

* improve:优化渠道调度稳定性

* improve:优化路由

* fix:修复项目预算控制缺陷

* fix:加强限流等安全措施

* feat:添加限流相关字段的前端展示及接口字段

* improve:优化注册时密码要求,只需要字母和数字即可

* feat(setting):添加注册限流设置

* improve:优化前端数字类型转换

* chore:rbac权限放行

---------

Co-authored-by: chengqian <wuchengqian@kkidc.com>
Co-authored-by: qian <wuchengqian5@163.com>
2026-06-23 22:47:54 +08:00

111 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"encoding/json"
"net/http"
"strings"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/logic/relay"
)
// ApiKeyAuth 是 API Key 认证中间件,用于 AI 代理端点。
// 支持以下认证方式(按优先级):
// - x-goog-api-key: sk-xxx(Gemini 原生格式)
// - Authorization: Bearer sk-xxx(OpenAI 兼容格式)
// - x-api-key: sk-xxx(Anthropic Claude 原生格式)
// - ?key=xxx query parameter(Gemini SDK 格式,仅 /v1beta 路径)
//
// 验证通过后注入 TenantID/UserID/ApiKeyID。
// 认证失败时使用供应商原生错误格式(/v1/messages 用 Claude 格式,其余用 OpenAI 格式)。
func ApiKeyAuth(r *ghttp.Request) {
tokenStr := strings.TrimSpace(r.GetHeader("x-goog-api-key"))
if tokenStr == "" {
tokenStr = extractBearerToken(r)
}
if tokenStr == "" {
tokenStr = strings.TrimSpace(r.GetHeader("x-api-key"))
}
// Gemini 原生 API 通过 query parameter 传递密钥:?key=xxx(仅 /v1beta 路径)
if tokenStr == "" && strings.HasPrefix(r.URL.Path, "/v1beta") {
tokenStr = strings.TrimSpace(r.Get("key").String())
}
if tokenStr == "" {
writeRelayAuthError(r, http.StatusUnauthorized, "authentication_error", "missing API key")
return
}
apiKeyInfo, err := relay.ValidateApiKey(r.Context(), tokenStr)
if err != nil {
code := http.StatusUnauthorized
errType := "authentication_error"
msg := "invalid API key"
switch err {
case consts.ErrKeyExpired:
errType = "authentication_error"
msg = "API key has expired"
case consts.ErrKeyDisabled:
code = http.StatusForbidden
errType = "permission_error"
msg = "API key is disabled"
case consts.ErrTenantSuspended:
code = http.StatusForbidden
errType = "permission_error"
msg = "tenant is suspended"
case consts.ErrProjectNotActive:
code = http.StatusForbidden
errType = "permission_error"
msg = "project is not active"
}
writeRelayAuthError(r, code, errType, msg)
return
}
// Set auth context
r.SetCtxVar(CtxKeyTenantID, apiKeyInfo.TenantID)
r.SetCtxVar(CtxKeyUserID, apiKeyInfo.UserID)
r.SetCtxVar(CtxKeyApiKeyID, apiKeyInfo.ID)
r.SetCtxVar(CtxKeyProjectID, apiKeyInfo.ProjectID)
r.SetCtxVar("ApiKeyScope", apiKeyInfo.Scope)
r.SetCtxVar(CtxKeyApiKeyRateLimitQps, apiKeyInfo.RateLimitQps)
r.SetCtxVar(CtxKeyApiKeyRateLimitConcurrency, apiKeyInfo.RateLimitConcurrency)
r.SetCtxVar(CtxKeyApiKeyIpWhitelist, apiKeyInfo.IpWhitelist)
r.SetCtxVar(CtxKeyApiKeyTotalQuota, apiKeyInfo.TotalQuota)
r.SetCtxVar(CtxKeyApiKeyUsedQuota, apiKeyInfo.UsedQuota)
r.Middleware.Next()
}
// writeRelayAuthError 写入供应商原生格式的认证错误响应
// /v1/messages 使用 Claude 格式,其余使用 OpenAI 格式
func writeRelayAuthError(r *ghttp.Request, statusCode int, errType, message string) {
r.Response.Header().Set("Content-Type", "application/json")
r.Response.WriteHeader(statusCode)
path := r.URL.Path
if path == "/v1/messages" {
// Claude 格式: {"type": "error", "error": {"type": "...", "message": "..."}}
body, _ := json.Marshal(map[string]any{
"type": "error",
"error": map[string]string{
"type": errType,
"message": message,
},
})
r.Response.Write(body)
} else {
// OpenAI 格式: {"error": {"type": "...", "message": "..."}}
body, _ := json.Marshal(map[string]any{
"error": map[string]string{
"type": errType,
"message": message,
},
})
r.Response.Write(body)
}
}