package pub import ( "context" "errors" "testing" "perms-system-server/internal/middleware" "perms-system-server/internal/response" "perms-system-server/internal/svc" "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" ) // --------------------------------------------------------------------------- // 覆盖目标:审计第 6 轮 H-1 修复回归 —— AdminLogin 限流按 `admin::` 双维。 // // H-1 攻击:攻击者只靠已知或枚举出的超管用户名 `admin_`,从任意远端 IP 连打 // 错误密码 → 触发 5 分钟封禁 → 合法超管任何 IP 都无法登录。修复后 key 上挂 clientIP: // 换 IP 的远端不继承上一桶计数,合法用户自身 IP 仍能进入。 // --------------------------------------------------------------------------- // newAdminLimitSvcCtx 构造一个挂了独立 UsernameLoginLimit (quota=1) 的 svcCtx, // 避免测试之间的限流状态串扰。返回的 svcCtx 可直接传给 NewAdminLoginLogic。 func newAdminLimitSvcCtx(t *testing.T, quota int) *svc.ServiceContext { t.Helper() cfg := testutil.GetTestConfig() rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf) svcCtx := newTestSvcCtx() svcCtx.UsernameLoginLimit = limit.NewPeriodLimit(300, quota, rds, cfg.CacheRedis.KeyPrefix+":rl:adminlogin:ut:"+testutil.UniqueId()) return svcCtx } // TC-0834: 同 IP + 同 username 超过 quota 必须 429,文案为新版本。 func TestAdminLogin_H1_SameIPSameUsername_OverQuota429(t *testing.T) { svcCtx := newAdminLimitSvcCtx(t, 1) username := "h1_user_" + testutil.UniqueId() ctx := middleware.WithClientIP(context.Background(), "1.2.3.4") req := &types.AdminLoginReq{ Username: username, Password: "bad", ManagementKey: svcCtx.Config.Auth.ManagementKey, } _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code(), "首次调用应被限流放行并进入业务层,得到 401") _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req) require.Error(t, err) require.True(t, errors.As(err, &ce)) assert.Equal(t, 429, ce.Code(), "同 IP+同 username 第二次必须 429") assert.Equal(t, "登录尝试过于频繁,请5分钟后再试", ce.Error()) } // TC-0835: 同 username 换远端 IP 不得继承配额。 func TestAdminLogin_H1_DifferentIPSameUsername_IndependentBucket(t *testing.T) { svcCtx := newAdminLimitSvcCtx(t, 1) username := "h1_iso_" + testutil.UniqueId() req := &types.AdminLoginReq{ Username: username, Password: "bad", ManagementKey: svcCtx.Config.Auth.ManagementKey, } ctxA := middleware.WithClientIP(context.Background(), "10.0.0.1") _, err := NewAdminLoginLogic(ctxA, svcCtx).AdminLogin(req) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) _, err = NewAdminLoginLogic(ctxA, svcCtx).AdminLogin(req) require.Error(t, err) require.True(t, errors.As(err, &ce)) assert.Equal(t, 429, ce.Code(), "IP-A 配额已满") ctxB := middleware.WithClientIP(context.Background(), "10.0.0.2") _, err = NewAdminLoginLogic(ctxB, svcCtx).AdminLogin(req) require.Error(t, err) require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code(), "H-1 修复:换远端 IP 必须命中独立限流桶,不能被同 username 的旧计数拖连") } // TC-0836: ctx 里无 clientIP —— 退化为 "unknown" 共享桶,仍能限流,不得绕过。 func TestAdminLogin_H1_MissingClientIP_FallbackBucket(t *testing.T) { svcCtx := newAdminLimitSvcCtx(t, 1) username := "h1_unk_" + testutil.UniqueId() req := &types.AdminLoginReq{ Username: username, Password: "bad", ManagementKey: svcCtx.Config.Auth.ManagementKey, } ctx := context.Background() _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req) require.Error(t, err) require.True(t, errors.As(err, &ce)) assert.Equal(t, 429, ce.Code(), "无 clientIP 时应该退化到 'unknown' 桶继续限流,严禁直接绕过") } // TC-0837: managementKey 错误路径不消耗 username quota(Take 顺序冻结)。 func TestAdminLogin_H1_BadManagementKey_DoesNotConsumeQuota(t *testing.T) { svcCtx := newAdminLimitSvcCtx(t, 1) username := "h1_mk_" + testutil.UniqueId() ctx := middleware.WithClientIP(context.Background(), "172.16.0.9") _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{ Username: username, Password: "whatever", ManagementKey: "WRONG-KEY", }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) assert.Equal(t, "managementKey无效", ce.Error()) _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{ Username: username, Password: "whatever", ManagementKey: svcCtx.Config.Auth.ManagementKey, }) require.Error(t, err) require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code(), "H-1 顺序:managementKey 错误应在 Take 之前 return,不应消耗 per-IP+user 配额") }