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,
|
|||
|
|
})
|
|||
|
|
}
|