111 lines
3.2 KiB
Go
111 lines
3.2 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
|
|
}
|
|
|
|
// 视频片段流接口仅放行播放器请求;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 <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
|
|
}
|
|
|
|
// 校验令牌是否已被登出吊销
|
|
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()
|
|
}
|
|
}
|