package middleware import ( "net/http" "strings" "github.com/edu-cloud/api-gateway/internal/config" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // publicPaths 是无需鉴权的公开路径(精确匹配,基于去掉 /api/v1 前缀后的路径) var publicPaths = map[string]bool{ "/iam/register": true, "/iam/login": true, "/iam/refresh": true, } // isPublicPath 判断请求路径是否属于公开路径(无需鉴权) // 匹配规则:去掉 /api/v1 前缀后,与 publicPaths 精确匹配 func isPublicPath(path string) bool { stripped := strings.TrimPrefix(path, "/api/v1") return publicPaths[stripped] } // AuthMiddleware 验证 JWT 并注入用户信息到请求头 // P1 用 HS256,P2 改 RS256(IAM 签发) func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { // 健康检查跳过鉴权 if c.Request.URL.Path == "/healthz" { c.Next() return } // 公开路径白名单(register/login/refresh 无需鉴权) if isPublicPath(c.Request.URL.Path) { c.Next() return } authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "success": false, "error": gin.H{ "code": "UNAUTHORIZED", "message": "missing authorization header", }, }) return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") if tokenStr == authHeader { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "success": false, "error": gin.H{ "code": "UNAUTHORIZED", "message": "invalid authorization scheme, expected Bearer", }, }) return } // 开发模式旁路:DEV_MODE=true 时接受 "dev-token",注入开发用户 // 仅用于本地联调,生产环境必须关闭 DEV_MODE if cfg.DevMode && tokenStr == "dev-token" { c.Request.Header.Set("x-user-id", "dev-user") c.Request.Header.Set("x-user-roles", "teacher,admin") c.Next() return } token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(cfg.JWTSecret), nil }) if err != nil || !token.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "success": false, "error": gin.H{ "code": "INVALID_TOKEN", "message": "token validation failed", }, }) return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "success": false, "error": gin.H{ "code": "INVALID_CLAIMS", "message": "invalid token claims", }, }) return } // 注入用户信息到下游请求头 if sub, ok := claims["sub"].(string); ok { c.Request.Header.Set("x-user-id", sub) } if roles, ok := claims["roles"].([]any); ok { roleStrs := make([]string, 0, len(roles)) for _, r := range roles { if s, ok := r.(string); ok { roleStrs = append(roleStrs, s) } } c.Request.Header.Set("x-user-roles", strings.Join(roleStrs, ",")) } c.Next() } }