admin_auth.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. // Package middleware provides HTTP middleware for authentication, authorization, and request processing.
  2. package middleware
  3. import (
  4. "crypto/subtle"
  5. "errors"
  6. "strings"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. // NewAdminAuthMiddleware 创建管理员认证中间件
  11. func NewAdminAuthMiddleware(
  12. authService *service.AuthService,
  13. userService *service.UserService,
  14. settingService *service.SettingService,
  15. auditService *service.AuditLogService,
  16. ) AdminAuthMiddleware {
  17. return AdminAuthMiddleware(adminAuth(authService, userService, settingService, auditService))
  18. }
  19. // adminAuth 管理员认证中间件实现
  20. // 支持两种认证方式(通过不同的 header 区分):
  21. // 1. Admin API Key: x-api-key: <admin-api-key>
  22. // 2. JWT Token: Authorization: Bearer <jwt-token> (需要管理员角色)
  23. func adminAuth(
  24. authService *service.AuthService,
  25. userService *service.UserService,
  26. settingService *service.SettingService,
  27. auditService *service.AuditLogService,
  28. ) gin.HandlerFunc {
  29. return func(c *gin.Context) {
  30. // WebSocket upgrade requests cannot set Authorization headers in browsers.
  31. // For admin WebSocket endpoints (e.g. Ops realtime), allow passing the JWT via
  32. // Sec-WebSocket-Protocol (subprotocol list) using a prefixed token item:
  33. // Sec-WebSocket-Protocol: sub2api-admin, jwt.<token>
  34. if isWebSocketUpgradeRequest(c) {
  35. if token := extractJWTFromWebSocketSubprotocol(c); token != "" {
  36. if !validateJWTForAdmin(c, token, authService, userService, settingService, auditService) {
  37. return
  38. }
  39. c.Next()
  40. return
  41. }
  42. }
  43. // 检查 x-api-key header(Admin API Key 认证)
  44. apiKey := c.GetHeader("x-api-key")
  45. if apiKey != "" {
  46. if !validateAdminAPIKey(c, apiKey, settingService, userService) {
  47. return
  48. }
  49. c.Next()
  50. return
  51. }
  52. // 检查 Authorization header(JWT 认证)
  53. authHeader := c.GetHeader("Authorization")
  54. if authHeader != "" {
  55. parts := strings.SplitN(authHeader, " ", 2)
  56. if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
  57. token := strings.TrimSpace(parts[1])
  58. if token == "" {
  59. AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
  60. return
  61. }
  62. if !validateJWTForAdmin(c, token, authService, userService, settingService, auditService) {
  63. return
  64. }
  65. c.Next()
  66. return
  67. }
  68. }
  69. // 无有效认证信息
  70. AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
  71. }
  72. }
  73. func isWebSocketUpgradeRequest(c *gin.Context) bool {
  74. if c == nil || c.Request == nil {
  75. return false
  76. }
  77. // RFC6455 handshake uses:
  78. // Connection: Upgrade
  79. // Upgrade: websocket
  80. upgrade := strings.ToLower(strings.TrimSpace(c.GetHeader("Upgrade")))
  81. if upgrade != "websocket" {
  82. return false
  83. }
  84. connection := strings.ToLower(c.GetHeader("Connection"))
  85. return strings.Contains(connection, "upgrade")
  86. }
  87. func extractJWTFromWebSocketSubprotocol(c *gin.Context) string {
  88. if c == nil {
  89. return ""
  90. }
  91. raw := strings.TrimSpace(c.GetHeader("Sec-WebSocket-Protocol"))
  92. if raw == "" {
  93. return ""
  94. }
  95. // The header is a comma-separated list of tokens. We reserve the prefix "jwt."
  96. // for carrying the admin JWT.
  97. for _, part := range strings.Split(raw, ",") {
  98. p := strings.TrimSpace(part)
  99. if strings.HasPrefix(p, "jwt.") {
  100. token := strings.TrimSpace(strings.TrimPrefix(p, "jwt."))
  101. if token != "" {
  102. return token
  103. }
  104. }
  105. }
  106. return ""
  107. }
  108. // validateAdminAPIKey 验证管理员 API Key
  109. func validateAdminAPIKey(
  110. c *gin.Context,
  111. key string,
  112. settingService *service.SettingService,
  113. userService *service.UserService,
  114. ) bool {
  115. storedKey, err := settingService.GetAdminAPIKey(c.Request.Context())
  116. if err != nil {
  117. AbortWithError(c, 500, "INTERNAL_ERROR", "Internal server error")
  118. return false
  119. }
  120. // 未配置或不匹配,统一返回相同错误(避免信息泄露)
  121. if storedKey == "" || subtle.ConstantTimeCompare([]byte(key), []byte(storedKey)) != 1 {
  122. AbortWithError(c, 401, "INVALID_ADMIN_KEY", "Invalid admin API key")
  123. return false
  124. }
  125. // 获取真实的管理员用户
  126. admin, err := userService.GetFirstAdmin(c.Request.Context())
  127. if err != nil {
  128. AbortWithError(c, 500, "INTERNAL_ERROR", "No admin user found")
  129. return false
  130. }
  131. c.Set(string(ContextKeyUser), AuthSubject{
  132. UserID: admin.ID,
  133. Concurrency: admin.Concurrency,
  134. })
  135. c.Set(string(ContextKeyUserRole), admin.Role)
  136. c.Set(ContextKeyAuthEmail, admin.Email)
  137. c.Set("auth_method", "admin_api_key")
  138. return true
  139. }
  140. // validateJWTForAdmin 验证 JWT 并检查管理员权限
  141. func validateJWTForAdmin(
  142. c *gin.Context,
  143. token string,
  144. authService *service.AuthService,
  145. userService *service.UserService,
  146. settingService *service.SettingService,
  147. auditService *service.AuditLogService,
  148. ) bool {
  149. // 验证 JWT token
  150. claims, err := authService.ValidateToken(token)
  151. if err != nil {
  152. if errors.Is(err, service.ErrTokenExpired) {
  153. AbortWithError(c, 401, "TOKEN_EXPIRED", "Token has expired")
  154. return false
  155. }
  156. AbortWithError(c, 401, "INVALID_TOKEN", "Invalid token")
  157. return false
  158. }
  159. // 从数据库获取用户
  160. user, err := userService.GetByID(c.Request.Context(), claims.UserID)
  161. if err != nil {
  162. AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
  163. return false
  164. }
  165. // 检查用户状态
  166. if !user.IsActive() {
  167. AbortWithError(c, 401, "USER_INACTIVE", "User account is not active")
  168. return false
  169. }
  170. // 校验 TokenVersion,确保管理员改密后旧 token 失效
  171. if claims.TokenVersion != user.TokenVersion {
  172. AbortWithError(c, 401, "TOKEN_REVOKED", "Token has been revoked (password changed)")
  173. return false
  174. }
  175. // 会话绑定校验:IP/UA 任一变化即撤销会话(功能可在系统设置中关闭)
  176. if !enforceSessionBinding(c, authService, settingService, auditService, claims) {
  177. return false
  178. }
  179. // 检查管理员权限
  180. if !user.IsAdmin() {
  181. AbortWithError(c, 403, "FORBIDDEN", "Admin access required")
  182. return false
  183. }
  184. c.Set(string(ContextKeyUser), AuthSubject{
  185. UserID: user.ID,
  186. Concurrency: user.Concurrency,
  187. })
  188. c.Set(string(ContextKeyUserRole), user.Role)
  189. c.Set(ContextKeyAuthEmail, user.Email)
  190. c.Set(ContextKeySessionID, claims.SessionID)
  191. c.Set("auth_method", "jwt")
  192. return true
  193. }