fix: 完善鉴权模块,修复 CI/CD 中 dubious ownership 错误 #143
168
backend/internal/store/cached_user.go
Normal file
168
backend/internal/store/cached_user.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
"github.com/hhs/camtalk/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Redis key 前缀。
|
||||||
|
const (
|
||||||
|
refreshTokenPrefix = "auth:refresh:" // auth:refresh:{token_hash} → user_id
|
||||||
|
userRefreshPrefix = "auth:user_refresh:" // auth:user_refresh:{user_id} → Set of token_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
// CachedUserRepository 装饰器,为 UserRepository 的 refresh token 操作增加 Redis 缓存。
|
||||||
|
// 读路径:Redis miss → DB → 回填 Redis。
|
||||||
|
// 写路径:同步双写 Redis + DB。
|
||||||
|
// 删路径:同步双删 Redis + DB。
|
||||||
|
// Redis 操作失败时降级到纯 DB,不阻断主流程。
|
||||||
|
type CachedUserRepository struct {
|
||||||
|
inner UserRepository
|
||||||
|
rdb *redis.Client
|
||||||
|
backfillTTL time.Duration // DB 回填 Redis 时使用的默认 TTL
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCachedUserRepository 创建带 Redis 缓存的 UserRepository 装饰器。
|
||||||
|
// backfillTTL: 从 DB 回填 Redis 时使用的 TTL(因 DB 接口不返回 expiresAt)。
|
||||||
|
func NewCachedUserRepository(inner UserRepository, rdb *redis.Client, backfillTTL time.Duration) *CachedUserRepository {
|
||||||
|
if backfillTTL <= 0 {
|
||||||
|
backfillTTL = 24 * time.Hour
|
||||||
|
}
|
||||||
|
return &CachedUserRepository{
|
||||||
|
inner: inner,
|
||||||
|
rdb: rdb,
|
||||||
|
backfillTTL: backfillTTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshTokenKey 生成 refresh token 的 Redis key。
|
||||||
|
func refreshTokenKey(tokenHash string) string {
|
||||||
|
return refreshTokenPrefix + tokenHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// userRefreshKey 生成用户 refresh token 集合的 Redis key。
|
||||||
|
func userRefreshKey(userID string) string {
|
||||||
|
return userRefreshPrefix + userID
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 委托方法(不做缓存) ---
|
||||||
|
|
||||||
|
func (r *CachedUserRepository) Create(ctx context.Context, username, passwordHash string) (string, error) {
|
||||||
|
return r.inner.Create(ctx, username, passwordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CachedUserRepository) FindByUsername(ctx context.Context, username string) (*User, error) {
|
||||||
|
return r.inner.FindByUsername(ctx, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CachedUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
|
||||||
|
return r.inner.FindByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 缓存方法 ---
|
||||||
|
|
||||||
|
// SaveRefreshToken Write-Through:先写 DB,再写 Redis。
|
||||||
|
func (r *CachedUserRepository) SaveRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error {
|
||||||
|
// 先写 DB
|
||||||
|
if err := r.inner.SaveRefreshToken(ctx, userID, tokenHash, expiresAt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写 Redis(SET + SADD),设置 TTL 为 token 剩余有效期
|
||||||
|
ttl := time.Until(expiresAt)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := refreshTokenKey(tokenHash)
|
||||||
|
pipe := r.rdb.Pipeline()
|
||||||
|
pipe.Set(ctx, key, userID, ttl)
|
||||||
|
pipe.SAdd(ctx, userRefreshKey(userID), tokenHash)
|
||||||
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
|
logger.Log.Warnw("Redis cache write failed for refresh token", "error", err)
|
||||||
|
// 降级:DB 已写入成功,Redis 失败不影响正确性
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindRefreshToken Read-Through:先查 Redis,miss 时查 DB 并回填。
|
||||||
|
func (r *CachedUserRepository) FindRefreshToken(ctx context.Context, tokenHash string) (string, error) {
|
||||||
|
key := refreshTokenKey(tokenHash)
|
||||||
|
|
||||||
|
// 查 Redis
|
||||||
|
userID, err := r.rdb.Get(ctx, key).Result()
|
||||||
|
if err == nil {
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
|
// redis.Nil 表示 key 不存在,其他错误记录日志后降级到 DB
|
||||||
|
if err != redis.Nil {
|
||||||
|
logger.Log.Warnw("Redis cache read failed for refresh token", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 降级到 DB
|
||||||
|
userID, err = r.inner.FindRefreshToken(ctx, tokenHash)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回填 Redis(SET + SADD),TTL 使用保守默认值
|
||||||
|
go func() {
|
||||||
|
bgCtx := context.Background()
|
||||||
|
pipe := r.rdb.Pipeline()
|
||||||
|
pipe.Set(bgCtx, key, userID, r.backfillTTL)
|
||||||
|
pipe.SAdd(bgCtx, userRefreshKey(userID), tokenHash)
|
||||||
|
_, _ = pipe.Exec(bgCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRefreshToken 双删:先删 DB,再删 Redis。
|
||||||
|
func (r *CachedUserRepository) DeleteRefreshToken(ctx context.Context, tokenHash string) error {
|
||||||
|
// 先从 Redis 获取 user_id(用于从集合中移除)
|
||||||
|
userID, _ := r.rdb.Get(ctx, refreshTokenKey(tokenHash)).Result()
|
||||||
|
|
||||||
|
// 删 DB
|
||||||
|
if err := r.inner.DeleteRefreshToken(ctx, tokenHash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删 Redis
|
||||||
|
key := refreshTokenKey(tokenHash)
|
||||||
|
pipe := r.rdb.Pipeline()
|
||||||
|
pipe.Del(ctx, key)
|
||||||
|
if userID != "" {
|
||||||
|
pipe.SRem(ctx, userRefreshKey(userID), tokenHash)
|
||||||
|
}
|
||||||
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
|
logger.Log.Warnw("Redis cache delete failed for refresh token", "error", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserRefreshTokens 批量清理:先从 Redis 获取集合,逐个删缓存,再删 DB。
|
||||||
|
func (r *CachedUserRepository) DeleteUserRefreshTokens(ctx context.Context, userID string) error {
|
||||||
|
userKey := userRefreshKey(userID)
|
||||||
|
|
||||||
|
// 从 Redis 获取该用户所有 token hash
|
||||||
|
hashes, _ := r.rdb.SMembers(ctx, userKey).Result()
|
||||||
|
|
||||||
|
// 批量删除 Redis 缓存
|
||||||
|
if len(hashes) > 0 {
|
||||||
|
keys := make([]string, 0, len(hashes)+1)
|
||||||
|
for _, h := range hashes {
|
||||||
|
keys = append(keys, refreshTokenKey(h))
|
||||||
|
}
|
||||||
|
keys = append(keys, userKey)
|
||||||
|
if err := r.rdb.Del(ctx, keys...).Err(); err != nil {
|
||||||
|
logger.Log.Warnw("Redis cache batch delete failed for user refresh tokens", "error", err, "userID", userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删 DB(无论 Redis 是否成功都执行)
|
||||||
|
return r.inner.DeleteUserRefreshTokens(ctx, userID)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user