middleware.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package middleware
  2. import (
  3. "context"
  4. "net/http"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. // ContextKey 定义上下文键类型
  11. type ContextKey string
  12. const (
  13. // ContextKeyUser 用户上下文键
  14. ContextKeyUser ContextKey = "user"
  15. // ContextKeyUserRole 当前用户角色(string)
  16. ContextKeyUserRole ContextKey = "user_role"
  17. // ContextKeyAPIKey API密钥上下文键
  18. ContextKeyAPIKey ContextKey = "api_key"
  19. // ContextKeySubscription 订阅上下文键
  20. ContextKeySubscription ContextKey = "subscription"
  21. // ContextKeyForcePlatform 强制平台(用于 /antigravity 路由)
  22. ContextKeyForcePlatform ContextKey = "force_platform"
  23. // ContextKeyOpsFallbackAPIKey 运维错误日志专用回退键。
  24. // 鉴权早退(分组停用/删除、Key 停用/过期/额度、用户停用、IP 限制等)时,
  25. // apiKey 已加载但尚未写入 ContextKeyAPIKey;该键让 Ops 错误日志仍能取到
  26. // user/group/platform。仅供 Ops 错误日志读取,不代表请求已通过鉴权。
  27. ContextKeyOpsFallbackAPIKey ContextKey = "ops_fallback_api_key"
  28. )
  29. // ForcePlatform 返回设置强制平台的中间件
  30. // 同时设置 request.Context(供 Service 使用)和 gin.Context(供 Handler 快速检查)
  31. func ForcePlatform(platform string) gin.HandlerFunc {
  32. return func(c *gin.Context) {
  33. // 设置到 request.Context,使用 ctxkey.ForcePlatform 供 Service 层读取
  34. ctx := context.WithValue(c.Request.Context(), ctxkey.ForcePlatform, platform)
  35. c.Request = c.Request.WithContext(ctx)
  36. // 同时设置到 gin.Context,供 Handler 快速检查
  37. c.Set(string(ContextKeyForcePlatform), platform)
  38. c.Next()
  39. }
  40. }
  41. // HasForcePlatform 检查是否有强制平台(用于 Handler 跳过分组检查)
  42. func HasForcePlatform(c *gin.Context) bool {
  43. _, exists := c.Get(string(ContextKeyForcePlatform))
  44. return exists
  45. }
  46. // GetForcePlatformFromContext 从 gin.Context 获取强制平台
  47. func GetForcePlatformFromContext(c *gin.Context) (string, bool) {
  48. value, exists := c.Get(string(ContextKeyForcePlatform))
  49. if !exists {
  50. return "", false
  51. }
  52. platform, ok := value.(string)
  53. return platform, ok
  54. }
  55. // ErrorResponse 标准错误响应结构
  56. type ErrorResponse struct {
  57. Code string `json:"code"`
  58. Message string `json:"message"`
  59. }
  60. // NewErrorResponse 创建错误响应
  61. func NewErrorResponse(code, message string) ErrorResponse {
  62. return ErrorResponse{
  63. Code: code,
  64. Message: message,
  65. }
  66. }
  67. // AbortWithError 中断请求并返回JSON错误
  68. func AbortWithError(c *gin.Context, statusCode int, code, message string) {
  69. c.JSON(statusCode, NewErrorResponse(code, message))
  70. c.Abort()
  71. }
  72. // abortWithOpenAIQuotaError writes the OpenAI-compatible insufficient quota response.
  73. func abortWithOpenAIQuotaError(c *gin.Context, statusCode int, message string) {
  74. c.JSON(statusCode, gin.H{
  75. "error": gin.H{
  76. "message": message,
  77. "type": "insufficient_quota",
  78. "param": nil,
  79. "code": "insufficient_quota",
  80. },
  81. })
  82. c.Abort()
  83. }
  84. // ──────────────────────────────────────────────────────────
  85. // RequireGroupAssignment — 未分组 Key 拦截中间件
  86. // ──────────────────────────────────────────────────────────
  87. // GatewayErrorWriter 定义网关错误响应格式(不同协议使用不同格式)
  88. type GatewayErrorWriter func(c *gin.Context, status int, message string)
  89. // AnthropicErrorWriter 按 Anthropic API 规范输出错误;error.type 随状态码映射
  90. // (404 -> not_found_error,403 -> permission_error,其余 api_error)。
  91. func AnthropicErrorWriter(c *gin.Context, status int, message string) {
  92. errorType := "api_error"
  93. switch status {
  94. case http.StatusNotFound:
  95. errorType = "not_found_error"
  96. case http.StatusForbidden:
  97. errorType = "permission_error"
  98. }
  99. c.JSON(status, gin.H{
  100. "type": "error",
  101. "error": gin.H{"type": errorType, "message": message},
  102. })
  103. }
  104. // GoogleErrorWriter 按 Google API 规范输出错误
  105. func GoogleErrorWriter(c *gin.Context, status int, message string) {
  106. c.JSON(status, gin.H{
  107. "error": gin.H{
  108. "code": status,
  109. "message": message,
  110. "status": googleapi.HTTPStatusToGoogleStatus(status),
  111. },
  112. })
  113. }
  114. // OpenAIErrorWriter 按 OpenAI API 规范输出模型级错误(model_not_found)。
  115. func OpenAIErrorWriter(c *gin.Context, status int, message string) {
  116. c.JSON(status, gin.H{
  117. "error": gin.H{
  118. "message": message,
  119. "type": "invalid_request_error",
  120. "code": "model_not_found",
  121. },
  122. })
  123. }
  124. // RequireGroupAssignment 检查 API Key 是否已分配到分组,
  125. // 如果未分组且系统设置不允许未分组 Key 调度则返回 403。
  126. func RequireGroupAssignment(settingService *service.SettingService, writeError GatewayErrorWriter) gin.HandlerFunc {
  127. return func(c *gin.Context) {
  128. apiKey, ok := GetAPIKeyFromContext(c)
  129. if !ok || apiKey.GroupID != nil {
  130. c.Next()
  131. return
  132. }
  133. // 未分组 Key — 检查系统设置
  134. if settingService.IsUngroupedKeySchedulingAllowed(c.Request.Context()) {
  135. c.Next()
  136. return
  137. }
  138. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonAPIKeyGroupUnassigned)
  139. MarkIngressRejected(c, IngressRejectGroupUnassigned)
  140. writeError(c, http.StatusForbidden, "API Key is not assigned to any group and cannot be used. Please contact the administrator to assign it to a group.")
  141. c.Abort()
  142. }
  143. }