refreshTokenRateLimit_audit_test.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. authHelper "perms-system-server/internal/logic/auth"
  7. "perms-system-server/internal/response"
  8. "perms-system-server/internal/testutil"
  9. "perms-system-server/internal/types"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "github.com/zeromicro/go-zero/core/limit"
  13. "github.com/zeromicro/go-zero/core/stores/redis"
  14. )
  15. // TC-0741: M-B 修复回归 —— /auth/refreshToken 必须受 TokenOpLimiter 保护,
  16. // 用 quota=1 的定制 limiter,同一用户第 2 次必须 429;
  17. // 且被限流的请求绝不能触发 IncrementTokenVersion(否则攻击者可持续废除 refresh 令牌)。
  18. func TestRefreshToken_TokenOpLimiter_BlocksBurst(t *testing.T) {
  19. ctx := context.Background()
  20. svcCtx := newTestSvcCtx()
  21. username := "rt_rl_" + testutil.UniqueId()
  22. password := "TestPass123"
  23. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  24. t.Cleanup(cleanUser)
  25. cfg := testutil.GetTestConfig()
  26. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  27. svcCtx.TokenOpLimiter = limit.NewPeriodLimit(60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:refresh:ut:"+testutil.UniqueId())
  28. mkReq := func(tv int64) *types.RefreshTokenReq {
  29. rt, err := authHelper.GenerateRefreshToken(
  30. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  31. userId, "", tv)
  32. require.NoError(t, err)
  33. return &types.RefreshTokenReq{Authorization: "Bearer " + rt}
  34. }
  35. resp1, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(0))
  36. require.NoError(t, err, "首次刷新应放行")
  37. require.NotNil(t, resp1)
  38. // DB tokenVersion 已变为 1,旧 claims.TokenVersion=0 的 refreshToken 已失效,
  39. // 所以第二次必须用新 token;但限流判定在 TokenVersion 校验之**后**、IncrementTokenVersion 之**前**,
  40. // 因此使用新版本号构造的 token 会先通过前置校验,再被 TokenOpLimiter 拦截。
  41. u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  42. require.NoError(t, err)
  43. tvAfterFirst := u.TokenVersion
  44. require.Equal(t, int64(1), tvAfterFirst)
  45. _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(tvAfterFirst))
  46. require.Error(t, err, "超限的第二次刷新必须被 429 拦截")
  47. var ce *response.CodeError
  48. require.True(t, errors.As(err, &ce))
  49. assert.Equal(t, 429, ce.Code())
  50. assert.Contains(t, ce.Error(), "过于频繁")
  51. uAfter, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  52. require.NoError(t, err)
  53. assert.Equal(t, tvAfterFirst, uAfter.TokenVersion,
  54. "被限流的 refresh 请求绝不可递增 tokenVersion")
  55. }
  56. // TC-0742: M-B 修复 —— 限流按用户粒度隔离(productCode 无关)。
  57. // 场景:同一用户连续两次带 productCode=空的刷新请求,若限流命中,不会影响其它用户。
  58. func TestRefreshToken_TokenOpLimiter_PerUserIsolated(t *testing.T) {
  59. ctx := context.Background()
  60. svcCtx := newTestSvcCtx()
  61. uaId, cleanA := insertRefreshTestUser(t, ctx, "rt_iso_a_"+testutil.UniqueId(), "TestPass123", 1, 2)
  62. t.Cleanup(cleanA)
  63. ubId, cleanB := insertRefreshTestUser(t, ctx, "rt_iso_b_"+testutil.UniqueId(), "TestPass123", 1, 2)
  64. t.Cleanup(cleanB)
  65. cfg := testutil.GetTestConfig()
  66. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  67. svcCtx.TokenOpLimiter = limit.NewPeriodLimit(60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:refresh:iso:"+testutil.UniqueId())
  68. mkReq := func(uid, tv int64) *types.RefreshTokenReq {
  69. rt, err := authHelper.GenerateRefreshToken(
  70. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  71. uid, "", tv)
  72. require.NoError(t, err)
  73. return &types.RefreshTokenReq{Authorization: "Bearer " + rt}
  74. }
  75. // A:两次刷新,第 2 次必 429
  76. _, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(uaId, 0))
  77. require.NoError(t, err)
  78. _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(uaId, 1))
  79. require.Error(t, err)
  80. var ce *response.CodeError
  81. require.True(t, errors.As(err, &ce))
  82. require.Equal(t, 429, ce.Code())
  83. // B 应当还能刷新
  84. respB, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(ubId, 0))
  85. require.NoError(t, err, "B 用户的限流桶应当独立于 A")
  86. require.NotNil(t, respB)
  87. }