refresh_store.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. // refresh 白名单存储(满足 token 包定义的 token.RefreshStore 接口)。
  2. //
  3. // 键空间:refresh:{jti} → payload(token.RefreshPayload 的 JSON 序列化)。
  4. // TTL = refresh 有效期(由 token.Issuer 按 config.RefreshTokenTTLDay 设定,写入时透传)。
  5. //
  6. // 接口契约(token 包,本实现须严格遵守):
  7. // - SetRefresh:写白名单(登录/rotate 签发 refresh 后调用);Redis 写失败必须上抛 error
  8. // ——token.Issuer.Issue 会因此整体失败,不放行"只有 access 没有 refresh 白名单"的半套凭证;
  9. // - GetRefresh:读白名单(rotate 前调用);键不存在返回 (nil, nil)(不区分"未签发"与"已轮换/过期",
  10. // 由 token.Rotate 统一按 ErrRefreshInvalid 收敛——防枚举"某 jti 是否存在");
  11. // Redis 故障返回 (nil, error),token.Rotate 同样按 ErrRefreshInvalid 处理(故障时前端可重试登录);
  12. // - DeleteRefresh:删白名单(rotate 删旧串 / 登出删当前串);幂等——键不存在不报错(DEL 天然幂等)。
  13. //
  14. // 键前缀 "refresh:" 硬编码在本文件(不暴露配置项):
  15. // - 与吊销黑名单前缀(config.Auth.RevocationKeyPrefix,可配置)刻意分开——
  16. // 两键空间语义不同(白名单=正向凭证 / 黑名单=负向撤销),合并成一个前缀易在运维清键时误伤。
  17. package redis
  18. import (
  19. "context"
  20. "fmt"
  21. "time"
  22. gredis "github.com/redis/go-redis/v9"
  23. "yuansiwei/unified-backend/internal/infra/errors"
  24. )
  25. // refreshKeyPrefix refresh 白名单键前缀(见包注释:与吊销前缀分离是刻意的)。
  26. const refreshKeyPrefix = "refresh:"
  27. // refreshKey 拼 refresh 白名单键。jti 为 token.Issuer 内部生成的 16 字节 hex(见 token 包 newJTI),
  28. // 已保证不含 Redis 键特殊字符(hex 字符集),无需额外转义。
  29. func refreshKey(jti string) string {
  30. return refreshKeyPrefix + jti
  31. }
  32. // RefreshStore refresh 白名单存储(*Client 直接实现,装配时把 *Client 当 token.RefreshStore 传入)。
  33. //
  34. // 入参:均见 token.RefreshStore 接口注释。
  35. // 出参:error 语义见上方包注释"接口契约"。
  36. // 说明:本类型不新增字段——键前缀已硬编码,TTL 由调用方(Issuer)透传,无需存配置。
  37. type RefreshStore struct {
  38. *Client
  39. }
  40. // NewRefreshStore 构造 refresh 白名单存储(装配入口)。
  41. func NewRefreshStore(rc *Client) *RefreshStore {
  42. return &RefreshStore{Client: rc}
  43. }
  44. // SetRefresh 写入 refresh 白名单。
  45. //
  46. // 入参:ctx / jti / payload / ttl(见 token.RefreshStore 接口)。
  47. // 出参:error——Redis SET 失败(连接故障/超时)时返回包装后的 errors.ErrDBError;
  48. // 调用方(token.Issuer.Issue)必须按"整体签发失败"处理。
  49. func (s *RefreshStore) SetRefresh(ctx context.Context, jti string, payload []byte, ttl time.Duration) error {
  50. if err := s.Client.Set(ctx, refreshKey(jti), payload, ttl).Err(); err != nil {
  51. return errors.WithCause(errors.ErrDBError, fmt.Errorf("redis: 写 refresh 白名单 jti=%s 失败: %w", jti, err))
  52. }
  53. return nil
  54. }
  55. // GetRefresh 读取 refresh 白名单。
  56. //
  57. // 入参:ctx / jti(见接口)。
  58. // 出参:
  59. // - payload:键存在时的 JSON 载荷;键不存在返回 nil(与错误区分——gredis.Nil 不是 error);
  60. // - error:Redis 故障时非 nil(token.Rotate 按 ErrRefreshInvalid 收敛,前端可重试登录)。
  61. func (s *RefreshStore) GetRefresh(ctx context.Context, jti string) ([]byte, error) {
  62. val, err := s.Client.Get(ctx, refreshKey(jti)).Bytes()
  63. if err != nil {
  64. if err == gredis.Nil {
  65. return nil, nil // 键不存在(未签发/已轮换/已过期/已登出)——不报错
  66. }
  67. return nil, errors.WithCause(errors.ErrDBError, fmt.Errorf("redis: 读 refresh 白名单 jti=%s 失败: %w", jti, err))
  68. }
  69. return val, nil
  70. }
  71. // DeleteRefresh 删除 refresh 白名单(幂等)。
  72. //
  73. // 入参:ctx / jti(见接口)。
  74. // 出参:error——Redis DEL 失败时返回;键不存在时 go-redis 返回 (1|0, nil),不报错(幂等)。
  75. func (s *RefreshStore) DeleteRefresh(ctx context.Context, jti string) error {
  76. if err := s.Client.Del(ctx, refreshKey(jti)).Err(); err != nil {
  77. return errors.WithCause(errors.ErrDBError, fmt.Errorf("redis: 删 refresh 白名单 jti=%s 失败: %w", jti, err))
  78. }
  79. return nil
  80. }