2026-06-14 17:07:12 +08:00
|
|
|
|
package auth
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义错误。
|
|
|
|
|
|
var (
|
|
|
|
|
|
ErrInvalidToken = errors.New("invalid or expired token")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-20 14:50:19 +08:00
|
|
|
|
// 令牌类型常量。
|
|
|
|
|
|
const (
|
|
|
|
|
|
TokenTypeAccess = "access"
|
|
|
|
|
|
TokenTypeRefresh = "refresh"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-14 17:07:12 +08:00
|
|
|
|
// Claims JWT 声明。
|
|
|
|
|
|
type Claims struct {
|
2026-06-20 14:50:19 +08:00
|
|
|
|
UserID string `json:"user_id"`
|
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
|
TokenType string `json:"token_type"`
|
2026-06-14 17:07:12 +08:00
|
|
|
|
jwt.RegisteredClaims
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TokenManager JWT 令牌管理器。
|
|
|
|
|
|
type TokenManager struct {
|
|
|
|
|
|
secret []byte
|
|
|
|
|
|
accessTTL time.Duration
|
|
|
|
|
|
refreshTTL time.Duration
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewTokenManager 创建 TokenManager。
|
|
|
|
|
|
// secret: JWT 签名密钥;accessTTL/refreshTTL: 令牌有效期。
|
|
|
|
|
|
func NewTokenManager(secret string, accessTTL, refreshTTL time.Duration) *TokenManager {
|
|
|
|
|
|
return &TokenManager{
|
|
|
|
|
|
secret: []byte(secret),
|
|
|
|
|
|
accessTTL: accessTTL,
|
|
|
|
|
|
refreshTTL: refreshTTL,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GeneratePair 生成 access + refresh 令牌对。
|
|
|
|
|
|
func (tm *TokenManager) GeneratePair(userID, username string) (access, refresh string, err error) {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
|
|
|
|
// access token
|
|
|
|
|
|
accessClaims := &Claims{
|
2026-06-20 14:50:19 +08:00
|
|
|
|
UserID: userID,
|
|
|
|
|
|
Username: username,
|
|
|
|
|
|
TokenType: TokenTypeAccess,
|
2026-06-14 17:07:12 +08:00
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(tm.accessTTL)),
|
|
|
|
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
|
|
|
|
Issuer: "camtalk",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
accessTkn := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
|
|
|
|
|
access, err = accessTkn.SignedString(tm.secret)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// refresh token(含唯一 token_id 用于 DB 关联)
|
|
|
|
|
|
tokenID := uuid.New().String()
|
|
|
|
|
|
refreshClaims := &Claims{
|
2026-06-20 14:50:19 +08:00
|
|
|
|
UserID: userID,
|
|
|
|
|
|
Username: username,
|
|
|
|
|
|
TokenType: TokenTypeRefresh,
|
2026-06-14 17:07:12 +08:00
|
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
|
|
ID: tokenID,
|
|
|
|
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(tm.refreshTTL)),
|
|
|
|
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
|
|
|
|
Issuer: "camtalk",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
refreshTkn := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
|
|
|
|
|
refresh, err = refreshTkn.SignedString(tm.secret)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidateAccess 校验 access token 并返回 Claims。
|
|
|
|
|
|
func (tm *TokenManager) ValidateAccess(tokenStr string) (*Claims, error) {
|
2026-06-20 14:50:19 +08:00
|
|
|
|
claims, err := tm.validate(tokenStr)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if claims.TokenType != TokenTypeAccess {
|
|
|
|
|
|
return nil, ErrInvalidToken
|
|
|
|
|
|
}
|
|
|
|
|
|
return claims, nil
|
2026-06-14 17:07:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ValidateRefresh 校验 refresh token 并返回 Claims。
|
|
|
|
|
|
func (tm *TokenManager) ValidateRefresh(tokenStr string) (*Claims, error) {
|
2026-06-20 14:50:19 +08:00
|
|
|
|
claims, err := tm.validate(tokenStr)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if claims.TokenType != TokenTypeRefresh {
|
|
|
|
|
|
return nil, ErrInvalidToken
|
|
|
|
|
|
}
|
|
|
|
|
|
return claims, nil
|
2026-06-14 17:07:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// validate 解析并校验 JWT。
|
|
|
|
|
|
func (tm *TokenManager) validate(tokenStr string) (*Claims, error) {
|
|
|
|
|
|
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
|
|
|
|
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
|
|
|
return nil, ErrInvalidToken
|
|
|
|
|
|
}
|
|
|
|
|
|
return tm.secret, nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, ErrInvalidToken
|
|
|
|
|
|
}
|
|
|
|
|
|
claims, ok := token.Claims.(*Claims)
|
|
|
|
|
|
if !ok || !token.Valid {
|
|
|
|
|
|
return nil, ErrInvalidToken
|
|
|
|
|
|
}
|
|
|
|
|
|
return claims, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HashToken 对 token 做 SHA256 哈希,用于 DB 存储。
|
|
|
|
|
|
func HashToken(token string) string {
|
|
|
|
|
|
h := sha256.Sum256([]byte(token))
|
|
|
|
|
|
return hex.EncodeToString(h[:])
|
|
|
|
|
|
}
|