| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- 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)
- }
|