package handler import ( "errors" "net/http" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) const videoTokenTTL = 5 * time.Minute var videoTokenSecret []byte // SetVideoTokenSecret 设置视频短时令牌签名密钥。 func SetVideoTokenSecret(secret string) { videoTokenSecret = []byte(secret) } // IssueVideoToken 签发绑定用户、资源类型和资源 ID 的短时视频令牌。 func IssueVideoToken(userID, resourceType, resourceID string, ttl time.Duration) (string, error) { if len(videoTokenSecret) == 0 { return "", errors.New("video token secret not configured") } now := time.Now() claims := jwt.MapClaims{ "sub": userID, "purpose": "video", "rtype": resourceType, "rid": resourceID, "iat": now.Unix(), "exp": now.Add(ttl).Unix(), } return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(videoTokenSecret) } // ValidateVideoToken 校验视频令牌的资源类型、资源 ID、用途和有效期。 func ValidateVideoToken(token, resourceType, resourceID string) error { claims, err := parseVideoToken(token) if err != nil { return err } return validateVideoClaims(claims, resourceType, resourceID, "") } // ValidateVideoTokenForUser 额外校验令牌所属用户,用于可以拿到当前 JWT 的调用方。 func ValidateVideoTokenForUser(token, resourceType, resourceID, userID string) error { claims, err := parseVideoToken(token) if err != nil { return err } return validateVideoClaims(claims, resourceType, resourceID, userID) } // requireVideoToken 校验流代理 URL 上的 videoToken,缺失返回 401,错误/过期/资源错配返回 403。 func requireVideoToken(c *gin.Context, resourceType, resourceID string) bool { token := c.Query("videoToken") if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing video token"}) return false } if err := ValidateVideoToken(token, resourceType, resourceID); err != nil { c.JSON(http.StatusForbidden, gin.H{"error": "invalid video token"}) return false } return true } func parseVideoToken(token string) (jwt.MapClaims, error) { if len(videoTokenSecret) == 0 { return nil, errors.New("video token secret not configured") } claims := jwt.MapClaims{} parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { return videoTokenSecret, nil }, jwt.WithValidMethods([]string{"HS256"})) if err != nil { return nil, err } if !parsed.Valid { return nil, errors.New("invalid video token") } return claims, nil } func validateVideoClaims(claims jwt.MapClaims, resourceType, resourceID, userID string) error { if claims["purpose"] != "video" { return errors.New("invalid video token purpose") } if claims["rtype"] != resourceType { return errors.New("video token resource type mismatch") } if claims["rid"] != resourceID { return errors.New("video token resource mismatch") } if userID != "" && claims["sub"] != userID { return errors.New("video token user mismatch") } return nil }