package middleware import ( "net/http" "strings" "silk-server-go/internal/config" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // JWTClaims JWT 负载,与 NestJS 签发格式一致 type JWTClaims struct { Username string `json:"username"` Role string `json:"role"` TokenType string `json:"tokenType,omitempty"` // access | refresh,空值视为 access(兼容旧令牌) jwt.RegisteredClaims } // 白名单路径,无需鉴权 var whitelist = map[string]bool{ "/api/v1/health": true, "/health": true, "/api/health": true, "/api/v1/auth/login": true, "/auth/login": true, "/api/v1/auth/register": true, "/auth/register": true, "/api/v1/auth/refresh": true, "/auth/refresh": true, "/api/v1/video/clips/internal": true, "/api/v1/video/recordings/internal/end": true, } // Auth JWT 鉴权中间件 func Auth(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path // 白名单路径跳过鉴权 if whitelist[path] { c.Next() return } // 视频片段流接口仅放行播放器请求;handler 必须校验 videoToken if strings.HasPrefix(path, "/api/v1/video/clips/") && strings.HasSuffix(path, "/stream") { c.Next() return } // 直播流转码代理接口仅放行播放器请求;handler 必须校验 videoToken if strings.HasPrefix(path, "/api/v1/video/cameras/") && (strings.HasSuffix(path, "/live/stream") || strings.HasSuffix(path, "/live/proxy")) { c.Next() return } // 解析 Authorization: Bearer authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未提供认证信息"}) return } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "认证格式错误"}) return } tokenStr := parts[1] // 用 JWTSecret 验证 token,仅允许 HS256 算法 claims := &JWTClaims{} token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { return []byte(cfg.JWTSecret), nil }, jwt.WithValidMethods([]string{"HS256"})) if err != nil || !token.Valid { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "认证失败"}) return } // 校验令牌是否已被登出吊销 revoked, err := IsRevoked(claims) if err != nil { c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"error": "认证状态服务不可用"}) return } if revoked { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "令牌已注销,请重新登录"}) return } // 拒绝使用 refresh 令牌访问业务接口 if claims.TokenType == "refresh" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "认证失败"}) return } // 将用户信息存入 context c.Set("user", map[string]interface{}{ "sub": claims.Subject, "username": claims.Username, "role": claims.Role, }) c.Next() } }