// refresh 白名单存储(满足 token 包定义的 token.RefreshStore 接口)。 // // 键空间:refresh:{jti} → payload(token.RefreshPayload 的 JSON 序列化)。 // TTL = refresh 有效期(由 token.Issuer 按 config.RefreshTokenTTLDay 设定,写入时透传)。 // // 接口契约(token 包,本实现须严格遵守): // - SetRefresh:写白名单(登录/rotate 签发 refresh 后调用);Redis 写失败必须上抛 error // ——token.Issuer.Issue 会因此整体失败,不放行"只有 access 没有 refresh 白名单"的半套凭证; // - GetRefresh:读白名单(rotate 前调用);键不存在返回 (nil, nil)(不区分"未签发"与"已轮换/过期", // 由 token.Rotate 统一按 ErrRefreshInvalid 收敛——防枚举"某 jti 是否存在"); // Redis 故障返回 (nil, error),token.Rotate 同样按 ErrRefreshInvalid 处理(故障时前端可重试登录); // - DeleteRefresh:删白名单(rotate 删旧串 / 登出删当前串);幂等——键不存在不报错(DEL 天然幂等)。 // // 键前缀 "refresh:" 硬编码在本文件(不暴露配置项): // - 与吊销黑名单前缀(config.Auth.RevocationKeyPrefix,可配置)刻意分开—— // 两键空间语义不同(白名单=正向凭证 / 黑名单=负向撤销),合并成一个前缀易在运维清键时误伤。 package redis import ( "context" "fmt" "time" gredis "github.com/redis/go-redis/v9" "yuansiwei/unified-backend/internal/infra/errors" ) // refreshKeyPrefix refresh 白名单键前缀(见包注释:与吊销前缀分离是刻意的)。 const refreshKeyPrefix = "refresh:" // refreshKey 拼 refresh 白名单键。jti 为 token.Issuer 内部生成的 16 字节 hex(见 token 包 newJTI), // 已保证不含 Redis 键特殊字符(hex 字符集),无需额外转义。 func refreshKey(jti string) string { return refreshKeyPrefix + jti } // RefreshStore refresh 白名单存储(*Client 直接实现,装配时把 *Client 当 token.RefreshStore 传入)。 // // 入参:均见 token.RefreshStore 接口注释。 // 出参:error 语义见上方包注释"接口契约"。 // 说明:本类型不新增字段——键前缀已硬编码,TTL 由调用方(Issuer)透传,无需存配置。 type RefreshStore struct { *Client } // NewRefreshStore 构造 refresh 白名单存储(装配入口)。 func NewRefreshStore(rc *Client) *RefreshStore { return &RefreshStore{Client: rc} } // SetRefresh 写入 refresh 白名单。 // // 入参:ctx / jti / payload / ttl(见 token.RefreshStore 接口)。 // 出参:error——Redis SET 失败(连接故障/超时)时返回包装后的 errors.ErrDBError; // 调用方(token.Issuer.Issue)必须按"整体签发失败"处理。 func (s *RefreshStore) SetRefresh(ctx context.Context, jti string, payload []byte, ttl time.Duration) error { if err := s.Client.Set(ctx, refreshKey(jti), payload, ttl).Err(); err != nil { return errors.WithCause(errors.ErrDBError, fmt.Errorf("redis: 写 refresh 白名单 jti=%s 失败: %w", jti, err)) } return nil } // GetRefresh 读取 refresh 白名单。 // // 入参:ctx / jti(见接口)。 // 出参: // - payload:键存在时的 JSON 载荷;键不存在返回 nil(与错误区分——gredis.Nil 不是 error); // - error:Redis 故障时非 nil(token.Rotate 按 ErrRefreshInvalid 收敛,前端可重试登录)。 func (s *RefreshStore) GetRefresh(ctx context.Context, jti string) ([]byte, error) { val, err := s.Client.Get(ctx, refreshKey(jti)).Bytes() if err != nil { if err == gredis.Nil { return nil, nil // 键不存在(未签发/已轮换/已过期/已登出)——不报错 } return nil, errors.WithCause(errors.ErrDBError, fmt.Errorf("redis: 读 refresh 白名单 jti=%s 失败: %w", jti, err)) } return val, nil } // DeleteRefresh 删除 refresh 白名单(幂等)。 // // 入参:ctx / jti(见接口)。 // 出参:error——Redis DEL 失败时返回;键不存在时 go-redis 返回 (1|0, nil),不报错(幂等)。 func (s *RefreshStore) DeleteRefresh(ctx context.Context, jti string) error { if err := s.Client.Del(ctx, refreshKey(jti)).Err(); err != nil { return errors.WithCause(errors.ErrDBError, fmt.Errorf("redis: 删 refresh 白名单 jti=%s 失败: %w", jti, err)) } return nil }