feat: 收口视频访问与摄像头密钥输出
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user