This commit is contained in:
wwweww
2026-02-23 20:36:21 +08:00
parent 4898aecd3b
commit fdbcde13b2
52 changed files with 11263 additions and 194 deletions
+90
View File
@@ -0,0 +1,90 @@
package utils
import (
"encoding/base64"
"encoding/json"
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
)
// JWKS (JSON Web Key Set) 结构
type JWKSKey struct {
Kty string `json:"kty"`
Use string `json:"use"`
Kid string `json:"kid"`
N string `json:"n,omitempty"`
E string `json:"e,omitempty"`
K string `json:"k,omitempty"` // 对称密钥
Alg string `json:"alg"`
}
type JWKS struct {
Keys []JWKSKey `json:"keys"`
}
// GenerateJWKSFromSecret 从密钥生成 JWKS(用于对称加密 HS256)
func GenerateJWKSFromSecret(secretKey string, keyID string) *JWKS {
// 对于 HS256,将密钥进行 base64 编码
encodedSecret := base64.RawURLEncoding.EncodeToString([]byte(secretKey))
return &JWKS{
Keys: []JWKSKey{
{
Kty: "oct",
Use: "sig",
Kid: keyID,
K: encodedSecret,
Alg: "HS256",
},
},
}
}
// GenerateJWKSEndpoint 生成可以被 Envoy 使用的 JWKS JSON
// 此端点应在 user-rpc 中暴露,URL 为 /.well-known/jwks.json
func GenerateJWKSEndpoint(secretKey string, keyID string) (string, error) {
if secretKey == "" {
return "", fmt.Errorf("secret key cannot be empty")
}
jwks := GenerateJWKSFromSecret(secretKey, keyID)
jsonData, err := json.MarshalIndent(jwks, "", " ")
if err != nil {
return "", err
}
return string(jsonData), nil
}
// TokenPayload 令牌负载
type TokenMetadata struct {
IssuedAt time.Time
ExpiresAt time.Time
Subject string // userId
Issuer string
Audience string
}
// ExtractTokenMetadata 从 token 中提取元数据(不验证签名)
func ExtractTokenMetadata(tokenString string) (*TokenMetadata, error) {
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, fmt.Errorf("invalid token claims type")
}
return &TokenMetadata{
IssuedAt: claims.IssuedAt.Time,
ExpiresAt: claims.ExpiresAt.Time,
Subject: claims.UserId,
Issuer: claims.Issuer,
Audience: "", // 如果需要,可以增加到 Claims 中
}, nil
}
+218 -5
View File
@@ -1,8 +1,14 @@
package utils
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/redis/go-redis/v9"
)
type TokenPayload struct {
@@ -10,11 +16,16 @@ type TokenPayload struct {
IsAdmin bool
}
type Claims struct {
TokenPayload
jwt.RegisteredClaims
}
const (
tokenCachePrefixUser = "jwt:user:"
tokenCachePrefixToken = "jwt:token:"
tokenCacheTTL = 60 * 24 * time.Hour
tokenLifetime = 5 * 24 * time.Hour
tokenCacheTTL = 30 * 24 * time.Hour
tokenLifetime = 7 * 24 * time.Hour
)
var (
@@ -22,9 +33,211 @@ var (
errInvalidToken = errors.New("invalid token claims")
errTokenNotInCache = errors.New("token not found in cache")
errNoRedisClient = errors.New("redis client not configured")
// errExpiredToken = errors.New("token expired")
)
func NewToken(payload TokenPayload) (string, error) {
return "", nil
type JwtManager struct {
redisCluster *redis.ClusterClient
secretKey string
issuer string
}
func NewJwtManager(redisCluster *redis.ClusterClient, secretKey, issuer string) *JwtManager {
return &JwtManager{
redisCluster: redisCluster,
secretKey: secretKey,
issuer: issuer,
}
}
// New 生成新的 JWT token
func (m *JwtManager) New(ctx context.Context, payload *TokenPayload) (string, error) {
if m.redisCluster == nil {
return "", errNoRedisClient
}
now := time.Now()
expiresAt := now.Add(tokenLifetime)
claims := &Claims{
TokenPayload: *payload,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
Issuer: m.issuer,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(m.secretKey))
if err != nil {
return "", err
}
// 存储 token 到 RedisTTL 为 30 天
userKey := tokenCachePrefixUser + payload.UserId
tokenKey := tokenCachePrefixToken + tokenString
tokenData, _ := json.Marshal(payload)
// 同时存储两个 key:用户 -> token 和 token -> payload
pipe := m.redisCluster.Pipeline()
pipe.Set(ctx, userKey, tokenString, tokenCacheTTL)
pipe.Set(ctx, tokenKey, string(tokenData), tokenCacheTTL)
_, err = pipe.Exec(ctx)
if err != nil {
return "", err
}
return tokenString, nil
}
// Valid 验证 token 有效性,支持自动换票
func (m *JwtManager) Valid(ctx context.Context, tokenString string) (*TokenPayload, error) {
if m.redisCluster == nil {
return nil, errNoRedisClient
}
if tokenString == "" {
return nil, errMissingToken
}
// 检查 token 是否在 Redis 中
tokenKey := tokenCachePrefixToken + tokenString
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
if err != nil && err != redis.Nil {
return nil, err
}
var payload TokenPayload
if err == redis.Nil {
return nil, errTokenNotInCache
}
err = json.Unmarshal([]byte(tokenData), &payload)
if err != nil {
return nil, errInvalidToken
}
// 解析 JWT 并验证签名和过期时间
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(m.secretKey), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errInvalidToken
}
return &claims.TokenPayload, nil
}
// Renew 换票逻辑:如果 token 过期但 Redis 中还存在,则生成新 token
func (m *JwtManager) Renew(ctx context.Context, tokenString string) (string, error) {
if m.redisCluster == nil {
return "", errNoRedisClient
}
// 检查 token 是否在 Redis 中(不检查过期时间)
tokenKey := tokenCachePrefixToken + tokenString
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
if err != nil {
if err == redis.Nil {
return "", errTokenNotInCache
}
return "", err
}
var payload TokenPayload
err = json.Unmarshal([]byte(tokenData), &payload)
if err != nil {
return "", errInvalidToken
}
// 删除旧 token 记录
userKey := tokenCachePrefixUser + payload.UserId
m.redisCluster.Del(ctx, tokenKey, userKey)
// 生成新 token
return m.New(ctx, &payload)
}
// extract payload from token without validating expiration (used for auto-renewal)
func (m *JwtManager) Extract(ctx context.Context, tokenString string) (*TokenPayload, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(m.secretKey), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, errInvalidToken
}
return &claims.TokenPayload, nil
}
// check if token exists in Redis (i.e. is valid and not revoked)
func (m *JwtManager) Exists(ctx context.Context, tokenString string) (bool, error) {
if m.redisCluster == nil {
return false, errNoRedisClient
}
tokenKey := tokenCachePrefixToken + tokenString
exists, err := m.redisCluster.Exists(ctx, tokenKey).Result()
if err != nil {
return false, err
}
return exists > 0, nil
}
// extract payload from JWT claims
func (m *JwtManager) ClaimsToPayload(claims *Claims) *TokenPayload {
return &claims.TokenPayload
}
// revoke token by deleting both user -> token and token -> payload keys from Redis
func (m *JwtManager) Revoke(ctx context.Context, tokenString string) error {
if m.redisCluster == nil {
return errNoRedisClient
}
payload, err := m.Extract(ctx, tokenString)
if err != nil {
return err
}
userKey := tokenCachePrefixUser + payload.UserId
tokenKey := tokenCachePrefixToken + tokenString
pipe := m.redisCluster.Pipeline()
pipe.Del(ctx, userKey)
pipe.Del(ctx, tokenKey)
_, err = pipe.Exec(ctx)
return err
}
func (m *JwtManager) GetUserToken(ctx context.Context, userID string) (string, error) {
if m.redisCluster == nil {
return "", errNoRedisClient
}
userKey := tokenCachePrefixUser + userID
token, err := m.redisCluster.Get(ctx, userKey).Result()
if err != nil {
if err == redis.Nil {
return "", fmt.Errorf("user %s has no token", userID)
}
return "", err
}
return token, nil
}