From 50734c5edc65f483556af1ba461acf5fb66c8d72 Mon Sep 17 00:00:00 2001 From: ianshaw Date: Thu, 25 Dec 2025 21:24:28 -0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E6=B7=BB=E5=8A=A0=20Google=20?= =?UTF-8?q?API=20Key=20=E8=AE=A4=E8=AF=81=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 api_key_auth_google.go: 支持 x-goog-api-key 格式认证 - 更新 api_key_auth.go: 适配 Gemini 原生 API 格式 --- .../server/middleware/api_key_auth.go | 19 ++- .../server/middleware/api_key_auth_google.go | 120 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 backend/internal/server/middleware/api_key_auth_google.go diff --git a/backend/internal/server/middleware/api_key_auth.go b/backend/internal/server/middleware/api_key_auth.go index 3a19e664..29abe27f 100644 --- a/backend/internal/server/middleware/api_key_auth.go +++ b/backend/internal/server/middleware/api_key_auth.go @@ -37,9 +37,24 @@ func apiKeyAuthWithSubscription(apiKeyService *service.ApiKeyService, subscripti apiKeyString = c.GetHeader("x-api-key") } - // 如果两个header都没有API key + // 如果x-api-key header中没有,尝试从x-goog-api-key header中提取(Gemini CLI兼容) if apiKeyString == "" { - AbortWithError(c, 401, "API_KEY_REQUIRED", "API key is required in Authorization header (Bearer scheme) or x-api-key header") + apiKeyString = c.GetHeader("x-goog-api-key") + } + + // 如果header中没有,尝试从query参数中提取(Google API key风格) + if apiKeyString == "" { + apiKeyString = c.Query("key") + } + + // 兼容常见别名 + if apiKeyString == "" { + apiKeyString = c.Query("api_key") + } + + // 如果所有header都没有API key + if apiKeyString == "" { + AbortWithError(c, 401, "API_KEY_REQUIRED", "API key is required in Authorization header (Bearer scheme), x-api-key header, x-goog-api-key header, or key/api_key query parameter") return } diff --git a/backend/internal/server/middleware/api_key_auth_google.go b/backend/internal/server/middleware/api_key_auth_google.go new file mode 100644 index 00000000..a2388598 --- /dev/null +++ b/backend/internal/server/middleware/api_key_auth_google.go @@ -0,0 +1,120 @@ +package middleware + +import ( + "errors" + "strings" + + "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ApiKeyAuthGoogle is a Google-style error wrapper for API key auth. +func ApiKeyAuthGoogle(apiKeyRepo ApiKeyAuthService) gin.HandlerFunc { + return ApiKeyAuthWithSubscriptionGoogle(apiKeyRepo, nil) +} + +// ApiKeyAuthWithSubscriptionGoogle behaves like ApiKeyAuthWithSubscription but returns Google-style errors: +// {"error":{"code":401,"message":"...","status":"UNAUTHENTICATED"}} +// +// It is intended for Gemini native endpoints (/v1beta) to match Gemini SDK expectations. +func ApiKeyAuthWithSubscriptionGoogle(apiKeyRepo ApiKeyAuthService, subscriptionService SubscriptionAuthService) gin.HandlerFunc { + return func(c *gin.Context) { + apiKeyString := extractAPIKeyFromRequest(c) + if apiKeyString == "" { + abortWithGoogleError(c, 401, "API key is required") + return + } + + apiKey, err := apiKeyRepo.GetByKey(c.Request.Context(), apiKeyString) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + abortWithGoogleError(c, 401, "Invalid API key") + return + } + abortWithGoogleError(c, 500, "Failed to validate API key") + return + } + + if !apiKey.IsActive() { + abortWithGoogleError(c, 401, "API key is disabled") + return + } + if apiKey.User == nil { + abortWithGoogleError(c, 401, "User associated with API key not found") + return + } + if !apiKey.User.IsActive() { + abortWithGoogleError(c, 401, "User account is not active") + return + } + + isSubscriptionType := apiKey.Group != nil && apiKey.Group.IsSubscriptionType() + if isSubscriptionType && subscriptionService != nil { + subscription, err := subscriptionService.GetActiveSubscription( + c.Request.Context(), + apiKey.User.ID, + apiKey.Group.ID, + ) + if err != nil { + abortWithGoogleError(c, 403, "No active subscription found for this group") + return + } + if err := subscriptionService.ValidateSubscription(c.Request.Context(), subscription); err != nil { + abortWithGoogleError(c, 403, err.Error()) + return + } + _ = subscriptionService.CheckAndActivateWindow(c.Request.Context(), subscription) + _ = subscriptionService.CheckAndResetWindows(c.Request.Context(), subscription) + if err := subscriptionService.CheckUsageLimits(c.Request.Context(), subscription, apiKey.Group, 0); err != nil { + abortWithGoogleError(c, 429, err.Error()) + return + } + c.Set(string(ContextKeySubscription), subscription) + } else { + if apiKey.User.Balance <= 0 { + abortWithGoogleError(c, 403, "Insufficient account balance") + return + } + } + + c.Set(string(ContextKeyApiKey), apiKey) + c.Set(string(ContextKeyUser), apiKey.User) + c.Next() + } +} + +func extractAPIKeyFromRequest(c *gin.Context) string { + authHeader := c.GetHeader("Authorization") + if authHeader != "" { + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 && parts[0] == "Bearer" && strings.TrimSpace(parts[1]) != "" { + return strings.TrimSpace(parts[1]) + } + } + if v := strings.TrimSpace(c.GetHeader("x-api-key")); v != "" { + return v + } + if v := strings.TrimSpace(c.GetHeader("x-goog-api-key")); v != "" { + return v + } + if v := strings.TrimSpace(c.Query("key")); v != "" { + return v + } + if v := strings.TrimSpace(c.Query("api_key")); v != "" { + return v + } + return "" +} + +func abortWithGoogleError(c *gin.Context, status int, message string) { + c.JSON(status, gin.H{ + "error": gin.H{ + "code": status, + "message": message, + "status": googleapi.HTTPStatusToGoogleStatus(status), + }, + }) + c.Abort() +}