package middleware import ( resellercontract "github.com/dujiao-next/internal/modules/reseller/contract" "net/http" "strconv" "strings" "time" "github.com/dujiao-next/internal/modules/identity/jwttoken" usercontract "github.com/dujiao-next/internal/modules/identity/user/contract" userauthapp "github.com/dujiao-next/internal/modules/identity/userauth/application" "go.uber.org/zap" "github.com/dujiao-next/internal/authz" "github.com/dujiao-next/internal/cache" "github.com/dujiao-next/internal/config" "github.com/dujiao-next/internal/constants" "github.com/dujiao-next/internal/i18n" "github.com/dujiao-next/internal/logger" admincontract "github.com/dujiao-next/internal/modules/identity/admin/contract" adminauthapp "github.com/dujiao-next/internal/modules/identity/adminauth/application" "github.com/dujiao-next/internal/platform/http/response" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) const requestIDKey = "request_id" const requestIDHeader = "X-Request-ID" const adminIsSuperContextKey = "admin_is_super" const authHeaderKey = "Authorization" const authSchemeBearer = "Bearer" // CORSMiddleware 跨域中间件 func CORSMiddleware(cfg config.CORSConfig) gin.HandlerFunc { allowedOrigins := cfg.AllowedOrigins if len(allowedOrigins) == 0 { allowedOrigins = config.DefaultCORSAllowedOrigins() } allowedMethods := cfg.AllowedMethods if len(allowedMethods) == 0 { allowedMethods = config.DefaultCORSAllowedMethods() } allowedHeaders := cfg.AllowedHeaders if len(allowedHeaders) == 0 { allowedHeaders = config.DefaultCORSAllowedHeaders() } methodsHeader := strings.Join(allowedMethods, ", ") headersHeader := strings.Join(allowedHeaders, ", ") return func(c *gin.Context) { origin := c.GetHeader("Origin") allowedOrigin := resolveAllowedOrigin(origin, allowedOrigins, cfg.AllowCredentials) if allowedOrigin != "" { c.Writer.Header().Set("Access-Control-Allow-Origin", allowedOrigin) if allowedOrigin != "*" { c.Writer.Header().Add("Vary", "Origin") } } if cfg.AllowCredentials { c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") } c.Writer.Header().Set("Access-Control-Allow-Headers", headersHeader) c.Writer.Header().Set("Access-Control-Allow-Methods", methodsHeader) if cfg.MaxAge > 0 { c.Writer.Header().Set("Access-Control-Max-Age", strconv.Itoa(cfg.MaxAge)) } if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } c.Next() } } func resolveAllowedOrigin(origin string, allowedOrigins []string, allowCredentials bool) string { if len(allowedOrigins) == 0 { return "" } for _, allowed := range allowedOrigins { if allowed == "*" { if allowCredentials && origin != "" { return origin } return "*" } } if origin == "" { return "" } for _, allowed := range allowedOrigins { if strings.EqualFold(allowed, origin) { return origin } } return "" } // RequestIDMiddleware 请求 ID 中间件 func RequestIDMiddleware() gin.HandlerFunc { return func(c *gin.Context) { requestID := strings.TrimSpace(c.GetHeader(requestIDHeader)) if requestID == "" { requestID = uuid.NewString() } c.Set(requestIDKey, requestID) c.Writer.Header().Set(requestIDHeader, requestID) c.Next() } } // LoggerMiddleware 结构化请求日志中间件 func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc { if logger == nil { logger = zap.L() } sugar := logger.Sugar() return func(c *gin.Context) { start := time.Now() c.Next() log := sugar.With( "request_id", getRequestID(c), "method", c.Request.Method, "path", c.Request.URL.Path, "status", c.Writer.Status(), "latency_ms", time.Since(start).Milliseconds(), "client_ip", c.ClientIP(), ) if len(c.Errors) > 0 { log.Errorw("request", "errors", c.Errors.String()) return } log.Infow("request") } } func getRequestID(c *gin.Context) string { value, ok := c.Get(requestIDKey) if !ok { return "" } if requestID, ok := value.(string); ok { return requestID } return "" } // JWTAuthMiddleware JWT 鉴权中间件 func JWTAuthMiddleware(secretKey string, adminRepo admincontract.Store) gin.HandlerFunc { return func(c *gin.Context) { if secretKey == "" { msg := i18n.T(i18n.ResolveLocale(c), "error.jwt_secret_missing") response.Unauthorized(c, msg) c.Abort() return } if adminRepo == nil { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } authHeader := c.GetHeader(authHeaderKey) if authHeader == "" { msg := i18n.T(i18n.ResolveLocale(c), "error.auth_header_missing") response.Unauthorized(c, msg) c.Abort() return } parts := strings.SplitN(authHeader, " ", 2) if !(len(parts) == 2 && parts[0] == authSchemeBearer) { msg := i18n.T(i18n.ResolveLocale(c), "error.auth_header_invalid") response.Unauthorized(c, msg) c.Abort() return } tokenString := parts[1] parser := newHS256JWTParser() claims := &adminauthapp.JWTClaims{} token, err := parser.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return []byte(secretKey), nil }) if err != nil || !token.Valid || claims.AdminID == 0 { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } if !jwttoken.IsAccessType(claims.Typ) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } if cached, hit, cacheErr := cache.GetAdminAuthState(c.Request.Context(), claims.AdminID); cacheErr == nil && hit && cached != nil { if claims.TokenVersion != cached.TokenVersion || !isIssuedAfterInvalidBeforeUnix(claims.IssuedAt, cached.TokenInvalidBefore) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_revoked") response.Unauthorized(c, msg) c.Abort() return } c.Set("admin_id", claims.AdminID) c.Set("username", claims.Username) c.Set(adminIsSuperContextKey, cached.IsSuper) c.Next() return } admin, err := adminRepo.GetByID(claims.AdminID) if err != nil || admin == nil { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } if claims.TokenVersion != admin.TokenVersion || !isIssuedAfterInvalidBefore(claims.IssuedAt, admin.TokenInvalidBefore) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_revoked") response.Unauthorized(c, msg) c.Abort() return } _ = cache.SetAdminAuthState(c.Request.Context(), cache.BuildAdminAuthState(admin)) c.Set("admin_id", claims.AdminID) c.Set("username", claims.Username) c.Set(adminIsSuperContextKey, admin.IsSuper) c.Next() } } // AdminRBACMiddleware 管理端 RBAC 鉴权中间件 func AdminRBACMiddleware(authzService *authz.Service) gin.HandlerFunc { return func(c *gin.Context) { if authzService == nil { logger.Errorw("admin_rbac_service_unavailable") msg := i18n.T(i18n.ResolveLocale(c), "error.unauthorized") response.Unauthorized(c, msg) c.Abort() return } if isSuper, ok := c.Get(adminIsSuperContextKey); ok { if superValue, typeOK := isSuper.(bool); typeOK && superValue { c.Next() return } } adminIDRaw, exists := c.Get("admin_id") if !exists { msg := i18n.T(i18n.ResolveLocale(c), "error.unauthorized") response.Unauthorized(c, msg) c.Abort() return } var adminID uint switch value := adminIDRaw.(type) { case uint: adminID = value case int: if value > 0 { adminID = uint(value) } case float64: if value > 0 { adminID = uint(value) } } if adminID == 0 { msg := i18n.T(i18n.ResolveLocale(c), "error.unauthorized") response.Unauthorized(c, msg) c.Abort() return } resource := c.FullPath() if strings.TrimSpace(resource) == "" { resource = c.Request.URL.Path } allowed, err := authzService.EnforceAdmin(adminID, resource, c.Request.Method) if err != nil { logger.Errorw("admin_rbac_enforce_failed", "admin_id", adminID, "method", c.Request.Method, "path", c.Request.URL.Path, "error", err, ) msg := i18n.T(i18n.ResolveLocale(c), "error.unauthorized") response.Unauthorized(c, msg) c.Abort() return } if !allowed { logger.Warnw("admin_rbac_permission_denied", "admin_id", adminID, "method", c.Request.Method, "path", c.Request.URL.Path, "resource", authz.NormalizeObject(resource), ) msg := i18n.T(i18n.ResolveLocale(c), "error.forbidden") response.Forbidden(c, msg) c.Abort() return } c.Next() } } // UserJWTAuthMiddleware 用户 JWT 鉴权中间件 func UserJWTAuthMiddleware(secretKey string, userRepo usercontract.Store) gin.HandlerFunc { return func(c *gin.Context) { if secretKey == "" { msg := i18n.T(i18n.ResolveLocale(c), "error.jwt_secret_missing") response.Unauthorized(c, msg) c.Abort() return } if userRepo == nil { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } authHeader := c.GetHeader(authHeaderKey) if authHeader == "" { msg := i18n.T(i18n.ResolveLocale(c), "error.auth_header_missing") response.Unauthorized(c, msg) c.Abort() return } parts := strings.SplitN(authHeader, " ", 2) if !(len(parts) == 2 && parts[0] == authSchemeBearer) { msg := i18n.T(i18n.ResolveLocale(c), "error.auth_header_invalid") response.Unauthorized(c, msg) c.Abort() return } tokenString := parts[1] parser := newHS256JWTParser() claims := &userauthapp.UserJWTClaims{} token, err := parser.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { return []byte(secretKey), nil }) if err != nil || !token.Valid || claims.UserID == 0 { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } if !jwttoken.IsAccessType(claims.Typ) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } // 令牌只在签发时所在的站点有效(主站 rid=0) if claims.ResellerID != resellercontract.RequestResellerID(c.Request.Context()) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } if cached, hit, cacheErr := cache.GetUserAuthState(c.Request.Context(), claims.UserID); cacheErr == nil && hit && cached != nil { if !isActiveUserStatus(cached.Status) { msg := i18n.T(i18n.ResolveLocale(c), "error.user_disabled") response.Unauthorized(c, msg) c.Abort() return } if claims.TokenVersion != cached.TokenVersion || !isIssuedAfterInvalidBeforeUnix(claims.IssuedAt, cached.TokenInvalidBefore) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_revoked") response.Unauthorized(c, msg) c.Abort() return } c.Set("user_id", claims.UserID) c.Set("user_email", claims.Email) c.Next() return } user, err := userRepo.GetByID(claims.UserID) if err != nil || user == nil { msg := i18n.T(i18n.ResolveLocale(c), "error.token_invalid") response.Unauthorized(c, msg) c.Abort() return } if !isActiveUserStatus(user.Status) { msg := i18n.T(i18n.ResolveLocale(c), "error.user_disabled") response.Unauthorized(c, msg) c.Abort() return } if claims.TokenVersion != user.TokenVersion || !isIssuedAfterInvalidBefore(claims.IssuedAt, user.TokenInvalidBefore) { msg := i18n.T(i18n.ResolveLocale(c), "error.token_revoked") response.Unauthorized(c, msg) c.Abort() return } _ = cache.SetUserAuthState(c.Request.Context(), cache.BuildUserAuthState(user)) c.Set("user_id", claims.UserID) c.Set("user_email", claims.Email) c.Next() } } func isIssuedAfterInvalidBefore(issuedAt *jwt.NumericDate, invalidBefore *time.Time) bool { if invalidBefore == nil { return true } if issuedAt == nil { return false } return issuedAt.Time.Unix() >= invalidBefore.Unix() } func isIssuedAfterInvalidBeforeUnix(issuedAt *jwt.NumericDate, invalidBeforeUnix int64) bool { if invalidBeforeUnix <= 0 { return true } if issuedAt == nil { return false } return issuedAt.Time.Unix() >= invalidBeforeUnix } func newHS256JWTParser() *jwt.Parser { return jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) } func isActiveUserStatus(status string) bool { return strings.ToLower(strings.TrimSpace(status)) == constants.UserStatusActive }