| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- // Package middleware provides HTTP middleware for authentication, authorization, and request processing.
- package middleware
- import (
- "crypto/subtle"
- "errors"
- "strings"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // NewAdminAuthMiddleware 创建管理员认证中间件
- func NewAdminAuthMiddleware(
- authService *service.AuthService,
- userService *service.UserService,
- settingService *service.SettingService,
- auditService *service.AuditLogService,
- ) AdminAuthMiddleware {
- return AdminAuthMiddleware(adminAuth(authService, userService, settingService, auditService))
- }
- // adminAuth 管理员认证中间件实现
- // 支持两种认证方式(通过不同的 header 区分):
- // 1. Admin API Key: x-api-key: <admin-api-key>
- // 2. JWT Token: Authorization: Bearer <jwt-token> (需要管理员角色)
- func adminAuth(
- authService *service.AuthService,
- userService *service.UserService,
- settingService *service.SettingService,
- auditService *service.AuditLogService,
- ) gin.HandlerFunc {
- return func(c *gin.Context) {
- // WebSocket upgrade requests cannot set Authorization headers in browsers.
- // For admin WebSocket endpoints (e.g. Ops realtime), allow passing the JWT via
- // Sec-WebSocket-Protocol (subprotocol list) using a prefixed token item:
- // Sec-WebSocket-Protocol: sub2api-admin, jwt.<token>
- if isWebSocketUpgradeRequest(c) {
- if token := extractJWTFromWebSocketSubprotocol(c); token != "" {
- if !validateJWTForAdmin(c, token, authService, userService, settingService, auditService) {
- return
- }
- c.Next()
- return
- }
- }
- // 检查 x-api-key header(Admin API Key 认证)
- apiKey := c.GetHeader("x-api-key")
- if apiKey != "" {
- if !validateAdminAPIKey(c, apiKey, settingService, userService) {
- return
- }
- c.Next()
- return
- }
- // 检查 Authorization header(JWT 认证)
- authHeader := c.GetHeader("Authorization")
- if authHeader != "" {
- parts := strings.SplitN(authHeader, " ", 2)
- if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
- token := strings.TrimSpace(parts[1])
- if token == "" {
- AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
- return
- }
- if !validateJWTForAdmin(c, token, authService, userService, settingService, auditService) {
- return
- }
- c.Next()
- return
- }
- }
- // 无有效认证信息
- AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
- }
- }
- func isWebSocketUpgradeRequest(c *gin.Context) bool {
- if c == nil || c.Request == nil {
- return false
- }
- // RFC6455 handshake uses:
- // Connection: Upgrade
- // Upgrade: websocket
- upgrade := strings.ToLower(strings.TrimSpace(c.GetHeader("Upgrade")))
- if upgrade != "websocket" {
- return false
- }
- connection := strings.ToLower(c.GetHeader("Connection"))
- return strings.Contains(connection, "upgrade")
- }
- func extractJWTFromWebSocketSubprotocol(c *gin.Context) string {
- if c == nil {
- return ""
- }
- raw := strings.TrimSpace(c.GetHeader("Sec-WebSocket-Protocol"))
- if raw == "" {
- return ""
- }
- // The header is a comma-separated list of tokens. We reserve the prefix "jwt."
- // for carrying the admin JWT.
- for _, part := range strings.Split(raw, ",") {
- p := strings.TrimSpace(part)
- if strings.HasPrefix(p, "jwt.") {
- token := strings.TrimSpace(strings.TrimPrefix(p, "jwt."))
- if token != "" {
- return token
- }
- }
- }
- return ""
- }
- // validateAdminAPIKey 验证管理员 API Key
- func validateAdminAPIKey(
- c *gin.Context,
- key string,
- settingService *service.SettingService,
- userService *service.UserService,
- ) bool {
- storedKey, err := settingService.GetAdminAPIKey(c.Request.Context())
- if err != nil {
- AbortWithError(c, 500, "INTERNAL_ERROR", "Internal server error")
- return false
- }
- // 未配置或不匹配,统一返回相同错误(避免信息泄露)
- if storedKey == "" || subtle.ConstantTimeCompare([]byte(key), []byte(storedKey)) != 1 {
- AbortWithError(c, 401, "INVALID_ADMIN_KEY", "Invalid admin API key")
- return false
- }
- // 获取真实的管理员用户
- admin, err := userService.GetFirstAdmin(c.Request.Context())
- if err != nil {
- AbortWithError(c, 500, "INTERNAL_ERROR", "No admin user found")
- return false
- }
- c.Set(string(ContextKeyUser), AuthSubject{
- UserID: admin.ID,
- Concurrency: admin.Concurrency,
- })
- c.Set(string(ContextKeyUserRole), admin.Role)
- c.Set(ContextKeyAuthEmail, admin.Email)
- c.Set("auth_method", "admin_api_key")
- return true
- }
- // validateJWTForAdmin 验证 JWT 并检查管理员权限
- func validateJWTForAdmin(
- c *gin.Context,
- token string,
- authService *service.AuthService,
- userService *service.UserService,
- settingService *service.SettingService,
- auditService *service.AuditLogService,
- ) bool {
- // 验证 JWT token
- claims, err := authService.ValidateToken(token)
- if err != nil {
- if errors.Is(err, service.ErrTokenExpired) {
- AbortWithError(c, 401, "TOKEN_EXPIRED", "Token has expired")
- return false
- }
- AbortWithError(c, 401, "INVALID_TOKEN", "Invalid token")
- return false
- }
- // 从数据库获取用户
- user, err := userService.GetByID(c.Request.Context(), claims.UserID)
- if err != nil {
- AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
- return false
- }
- // 检查用户状态
- if !user.IsActive() {
- AbortWithError(c, 401, "USER_INACTIVE", "User account is not active")
- return false
- }
- // 校验 TokenVersion,确保管理员改密后旧 token 失效
- if claims.TokenVersion != user.TokenVersion {
- AbortWithError(c, 401, "TOKEN_REVOKED", "Token has been revoked (password changed)")
- return false
- }
- // 会话绑定校验:IP/UA 任一变化即撤销会话(功能可在系统设置中关闭)
- if !enforceSessionBinding(c, authService, settingService, auditService, claims) {
- return false
- }
- // 检查管理员权限
- if !user.IsAdmin() {
- AbortWithError(c, 403, "FORBIDDEN", "Admin access required")
- return false
- }
- c.Set(string(ContextKeyUser), AuthSubject{
- UserID: user.ID,
- Concurrency: user.Concurrency,
- })
- c.Set(string(ContextKeyUserRole), user.Role)
- c.Set(ContextKeyAuthEmail, user.Email)
- c.Set(ContextKeySessionID, claims.SessionID)
- c.Set("auth_method", "jwt")
- return true
- }
|