204 lines
5.7 KiB
Go
204 lines
5.7 KiB
Go
package centrifugo
|
||
|
||
import (
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// TokenConfig JWT Token 配置
|
||
type TokenConfig struct {
|
||
// Secret 用於簽名的密鑰(與 Centrifugo 配置的 token_hmac_secret_key 一致)
|
||
Secret string
|
||
// ExpireIn Token 過期時間(預設 1 小時)
|
||
ExpireIn time.Duration
|
||
}
|
||
|
||
// TokenGenerator JWT Token 生成器
|
||
type TokenGenerator struct {
|
||
config TokenConfig
|
||
}
|
||
|
||
// NewTokenGenerator 創建新的 Token 生成器
|
||
func NewTokenGenerator(secret string) *TokenGenerator {
|
||
return &TokenGenerator{
|
||
config: TokenConfig{
|
||
Secret: secret,
|
||
ExpireIn: time.Hour,
|
||
},
|
||
}
|
||
}
|
||
|
||
// NewTokenGeneratorWithConfig 創建使用自定義配置的 Token 生成器
|
||
func NewTokenGeneratorWithConfig(config TokenConfig) *TokenGenerator {
|
||
if config.ExpireIn == 0 {
|
||
config.ExpireIn = time.Hour
|
||
}
|
||
return &TokenGenerator{
|
||
config: config,
|
||
}
|
||
}
|
||
|
||
// ConnectionClaims 連線 Token 的 Claims
|
||
type ConnectionClaims struct {
|
||
jwt.RegisteredClaims
|
||
// Sub 用戶 ID(必填)
|
||
Sub string `json:"sub"`
|
||
// Info 用戶資訊(可選,會在 presence 中顯示)
|
||
Info map[string]interface{} `json:"info,omitempty"`
|
||
// Channels 自動訂閱的頻道列表(可選)
|
||
Channels []string `json:"channels,omitempty"`
|
||
// TokenVersion Token 版本(用於批量撤銷)
|
||
TokenVersion int64 `json:"tv,omitempty"`
|
||
}
|
||
|
||
// SubscriptionClaims 訂閱 Token 的 Claims(用於私有頻道)
|
||
type SubscriptionClaims struct {
|
||
jwt.RegisteredClaims
|
||
// Sub 用戶 ID(必填)
|
||
Sub string `json:"sub"`
|
||
// Channel 頻道名稱(必填)
|
||
Channel string `json:"channel"`
|
||
// Info 頻道特定的用戶資訊(可選)
|
||
Info map[string]interface{} `json:"info,omitempty"`
|
||
// TokenVersion Token 版本(用於批量撤銷)
|
||
TokenVersion int64 `json:"tv,omitempty"`
|
||
}
|
||
|
||
// ConnectionTokenOptions 連線 Token 選項
|
||
type ConnectionTokenOptions struct {
|
||
// UserID 用戶 ID(必填)
|
||
UserID string
|
||
// Info 用戶資訊(可選)
|
||
Info map[string]interface{}
|
||
// Channels 自動訂閱的頻道列表(可選)
|
||
Channels []string
|
||
// ExpireAt 自定義過期時間(可選,為空則使用預設)
|
||
ExpireAt *time.Time
|
||
// TokenVersion Token 版本(可選,用於黑名單機制)
|
||
TokenVersion int64
|
||
}
|
||
|
||
// SubscriptionTokenOptions 訂閱 Token 選項
|
||
type SubscriptionTokenOptions struct {
|
||
// UserID 用戶 ID(必填)
|
||
UserID string
|
||
// Channel 頻道名稱(必填)
|
||
Channel string
|
||
// Info 頻道特定的用戶資訊(可選)
|
||
Info map[string]interface{}
|
||
// ExpireAt 自定義過期時間(可選,為空則使用預設)
|
||
ExpireAt *time.Time
|
||
// TokenVersion Token 版本(可選,用於黑名單機制)
|
||
TokenVersion int64
|
||
}
|
||
|
||
// TokenResult 生成 Token 的結果
|
||
type TokenResult struct {
|
||
Token string // JWT Token 字串
|
||
JTI string // JWT ID(用於撤銷單一 Token)
|
||
ExpiresAt time.Time // 過期時間
|
||
}
|
||
|
||
// GenerateConnectionToken 生成連線 Token
|
||
// 用於前端建立 WebSocket 連線時的身份驗證
|
||
func (g *TokenGenerator) GenerateConnectionToken(opts ConnectionTokenOptions) (string, error) {
|
||
result, err := g.GenerateConnectionTokenWithJTI(opts)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return result.Token, nil
|
||
}
|
||
|
||
// GenerateConnectionTokenWithJTI 生成連線 Token 並返回 JTI
|
||
// 用於需要支援單一 Token 撤銷的場景
|
||
func (g *TokenGenerator) GenerateConnectionTokenWithJTI(opts ConnectionTokenOptions) (*TokenResult, error) {
|
||
now := time.Now()
|
||
expireAt := now.Add(g.config.ExpireIn)
|
||
if opts.ExpireAt != nil {
|
||
expireAt = *opts.ExpireAt
|
||
}
|
||
|
||
// 生成唯一的 JTI
|
||
jti := uuid.New().String()
|
||
|
||
// 如果沒有指定 TokenVersion,使用當前時間戳
|
||
tokenVersion := opts.TokenVersion
|
||
if tokenVersion == 0 {
|
||
tokenVersion = now.UnixNano()
|
||
}
|
||
|
||
claims := ConnectionClaims{
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
ID: jti,
|
||
Subject: opts.UserID,
|
||
IssuedAt: jwt.NewNumericDate(now),
|
||
ExpiresAt: jwt.NewNumericDate(expireAt),
|
||
},
|
||
Sub: opts.UserID,
|
||
Info: opts.Info,
|
||
Channels: opts.Channels,
|
||
TokenVersion: tokenVersion,
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
tokenStr, err := token.SignedString([]byte(g.config.Secret))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &TokenResult{
|
||
Token: tokenStr,
|
||
JTI: jti,
|
||
ExpiresAt: expireAt,
|
||
}, nil
|
||
}
|
||
|
||
// GenerateSubscriptionToken 生成訂閱 Token
|
||
// 用於訂閱私有頻道時的身份驗證
|
||
func (g *TokenGenerator) GenerateSubscriptionToken(opts SubscriptionTokenOptions) (string, error) {
|
||
now := time.Now()
|
||
expireAt := now.Add(g.config.ExpireIn)
|
||
if opts.ExpireAt != nil {
|
||
expireAt = *opts.ExpireAt
|
||
}
|
||
|
||
claims := SubscriptionClaims{
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
Subject: opts.UserID,
|
||
IssuedAt: jwt.NewNumericDate(now),
|
||
ExpiresAt: jwt.NewNumericDate(expireAt),
|
||
},
|
||
Sub: opts.UserID,
|
||
Channel: opts.Channel,
|
||
Info: opts.Info,
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString([]byte(g.config.Secret))
|
||
}
|
||
|
||
// GenerateAnonymousToken 生成匿名連線 Token
|
||
// 用於允許匿名用戶連線(但仍需要驗證)
|
||
func (g *TokenGenerator) GenerateAnonymousToken() (string, error) {
|
||
return g.GenerateConnectionToken(ConnectionTokenOptions{
|
||
UserID: "", // 空的 UserID 表示匿名用戶
|
||
})
|
||
}
|
||
|
||
// QuickConnectionToken 快速生成連線 Token(只需用戶 ID)
|
||
func (g *TokenGenerator) QuickConnectionToken(userID string) (string, error) {
|
||
return g.GenerateConnectionToken(ConnectionTokenOptions{
|
||
UserID: userID,
|
||
})
|
||
}
|
||
|
||
// QuickSubscriptionToken 快速生成訂閱 Token(只需用戶 ID 和頻道)
|
||
func (g *TokenGenerator) QuickSubscriptionToken(userID, channel string) (string, error) {
|
||
return g.GenerateSubscriptionToken(SubscriptionTokenOptions{
|
||
UserID: userID,
|
||
Channel: channel,
|
||
})
|
||
}
|