package pub import ( "context" "errors" "testing" authHelper "perms-system-server/internal/logic/auth" "perms-system-server/internal/response" "perms-system-server/internal/testutil" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/limit" "github.com/zeromicro/go-zero/core/stores/redis" ) // TC-0741: M-B 修复回归 —— /auth/refreshToken 必须受 TokenOpLimiter 保护, // 用 quota=1 的定制 limiter,同一用户第 2 次必须 429; // 且被限流的请求绝不能触发 IncrementTokenVersion(否则攻击者可持续废除 refresh 令牌)。 func TestRefreshToken_TokenOpLimiter_BlocksBurst(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() username := "rt_rl_" + testutil.UniqueId() password := "TestPass123" userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2) t.Cleanup(cleanUser) cfg := testutil.GetTestConfig() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) svcCtx.TokenOpLimiter = limit.NewPeriodLimit(60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:refresh:ut:"+testutil.UniqueId()) mkReq := func(tv int64) *types.RefreshTokenReq { rt, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, userId, "", tv) require.NoError(t, err) return &types.RefreshTokenReq{Authorization: "Bearer " + rt} } resp1, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(0)) require.NoError(t, err, "首次刷新应放行") require.NotNil(t, resp1) // DB tokenVersion 已变为 1,旧 claims.TokenVersion=0 的 refreshToken 已失效, // 所以第二次必须用新 token;但限流判定在 TokenVersion 校验之**后**、IncrementTokenVersion 之**前**, // 因此使用新版本号构造的 token 会先通过前置校验,再被 TokenOpLimiter 拦截。 u, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) tvAfterFirst := u.TokenVersion require.Equal(t, int64(1), tvAfterFirst) _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(tvAfterFirst)) require.Error(t, err, "超限的第二次刷新必须被 429 拦截") var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 429, ce.Code()) assert.Contains(t, ce.Error(), "过于频繁") uAfter, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, tvAfterFirst, uAfter.TokenVersion, "被限流的 refresh 请求绝不可递增 tokenVersion") } // TC-0742: M-B 修复 —— 限流按用户粒度隔离(productCode 无关)。 // 场景:同一用户连续两次带 productCode=空的刷新请求,若限流命中,不会影响其它用户。 func TestRefreshToken_TokenOpLimiter_PerUserIsolated(t *testing.T) { ctx := context.Background() svcCtx := newTestSvcCtx() uaId, cleanA := insertRefreshTestUser(t, ctx, "rt_iso_a_"+testutil.UniqueId(), "TestPass123", 1, 2) t.Cleanup(cleanA) ubId, cleanB := insertRefreshTestUser(t, ctx, "rt_iso_b_"+testutil.UniqueId(), "TestPass123", 1, 2) t.Cleanup(cleanB) cfg := testutil.GetTestConfig() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) svcCtx.TokenOpLimiter = limit.NewPeriodLimit(60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:refresh:iso:"+testutil.UniqueId()) mkReq := func(uid, tv int64) *types.RefreshTokenReq { rt, err := authHelper.GenerateRefreshToken( svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire, uid, "", tv) require.NoError(t, err) return &types.RefreshTokenReq{Authorization: "Bearer " + rt} } // A:两次刷新,第 2 次必 429 _, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(uaId, 0)) require.NoError(t, err) _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(uaId, 1)) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) require.Equal(t, 429, ce.Code()) // B 应当还能刷新 respB, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(ubId, 0)) require.NoError(t, err, "B 用户的限流桶应当独立于 A") require.NotNil(t, respB) }