Files
silk/server-go/internal/middleware/auth.go
T

106 lines
3.0 KiB
Go

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
}
// 视频片段流接口支持动态 ID,使用前缀+后缀匹配放行
if strings.HasPrefix(path, "/api/v1/video/clips/") && strings.HasSuffix(path, "/stream") {
c.Next()
return
}
// 直播流转码代理接口
if strings.HasPrefix(path, "/api/v1/video/cameras/") && (strings.HasSuffix(path, "/live/stream") || strings.HasSuffix(path, "/live/proxy")) {
c.Next()
return
}
// 解析 Authorization: Bearer <token>
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
}
// 校验令牌是否已被登出吊销
if IsRevoked(claims) {
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()
}
}