| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- // 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
- }
|