adminLoginIpLimit_audit_test.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "perms-system-server/internal/middleware"
  7. "perms-system-server/internal/response"
  8. "perms-system-server/internal/svc"
  9. "perms-system-server/internal/testutil"
  10. "perms-system-server/internal/types"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. "github.com/zeromicro/go-zero/core/limit"
  14. "github.com/zeromicro/go-zero/core/stores/redis"
  15. )
  16. // ---------------------------------------------------------------------------
  17. // 覆盖目标:审计第 6 轮 H-1 修复回归 —— AdminLogin 限流按 `admin:<clientIP>:<username>` 双维。
  18. //
  19. // H-1 攻击:攻击者只靠已知或枚举出的超管用户名 `admin_<productCode>`,从任意远端 IP 连打
  20. // 错误密码 → 触发 5 分钟封禁 → 合法超管任何 IP 都无法登录。修复后 key 上挂 clientIP:
  21. // 换 IP 的远端不继承上一桶计数,合法用户自身 IP 仍能进入。
  22. // ---------------------------------------------------------------------------
  23. // newAdminLimitSvcCtx 构造一个挂了独立 UsernameLoginLimit (quota=1) 的 svcCtx,
  24. // 避免测试之间的限流状态串扰。返回的 svcCtx 可直接传给 NewAdminLoginLogic。
  25. func newAdminLimitSvcCtx(t *testing.T, quota int) *svc.ServiceContext {
  26. t.Helper()
  27. cfg := testutil.GetTestConfig()
  28. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  29. svcCtx := newTestSvcCtx()
  30. svcCtx.UsernameLoginLimit = limit.NewPeriodLimit(300, quota, rds,
  31. cfg.CacheRedis.KeyPrefix+":rl:adminlogin:ut:"+testutil.UniqueId())
  32. return svcCtx
  33. }
  34. // TC-0834: 同 IP + 同 username 超过 quota 必须 429,文案为新版本。
  35. func TestAdminLogin_H1_SameIPSameUsername_OverQuota429(t *testing.T) {
  36. svcCtx := newAdminLimitSvcCtx(t, 1)
  37. username := "h1_user_" + testutil.UniqueId()
  38. ctx := middleware.WithClientIP(context.Background(), "1.2.3.4")
  39. req := &types.AdminLoginReq{
  40. Username: username,
  41. Password: "bad",
  42. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  43. }
  44. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  45. require.Error(t, err)
  46. var ce *response.CodeError
  47. require.True(t, errors.As(err, &ce))
  48. assert.Equal(t, 401, ce.Code(), "首次调用应被限流放行并进入业务层,得到 401")
  49. _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  50. require.Error(t, err)
  51. require.True(t, errors.As(err, &ce))
  52. assert.Equal(t, 429, ce.Code(), "同 IP+同 username 第二次必须 429")
  53. assert.Equal(t, "登录尝试过于频繁,请5分钟后再试", ce.Error())
  54. }
  55. // TC-0835: 同 username 换远端 IP 不得继承配额。
  56. func TestAdminLogin_H1_DifferentIPSameUsername_IndependentBucket(t *testing.T) {
  57. svcCtx := newAdminLimitSvcCtx(t, 1)
  58. username := "h1_iso_" + testutil.UniqueId()
  59. req := &types.AdminLoginReq{
  60. Username: username,
  61. Password: "bad",
  62. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  63. }
  64. ctxA := middleware.WithClientIP(context.Background(), "10.0.0.1")
  65. _, err := NewAdminLoginLogic(ctxA, svcCtx).AdminLogin(req)
  66. require.Error(t, err)
  67. var ce *response.CodeError
  68. require.True(t, errors.As(err, &ce))
  69. assert.Equal(t, 401, ce.Code())
  70. _, err = NewAdminLoginLogic(ctxA, svcCtx).AdminLogin(req)
  71. require.Error(t, err)
  72. require.True(t, errors.As(err, &ce))
  73. assert.Equal(t, 429, ce.Code(), "IP-A 配额已满")
  74. ctxB := middleware.WithClientIP(context.Background(), "10.0.0.2")
  75. _, err = NewAdminLoginLogic(ctxB, svcCtx).AdminLogin(req)
  76. require.Error(t, err)
  77. require.True(t, errors.As(err, &ce))
  78. assert.Equal(t, 401, ce.Code(),
  79. "H-1 修复:换远端 IP 必须命中独立限流桶,不能被同 username 的旧计数拖连")
  80. }
  81. // TC-0836: ctx 里无 clientIP —— 退化为 "unknown" 共享桶,仍能限流,不得绕过。
  82. func TestAdminLogin_H1_MissingClientIP_FallbackBucket(t *testing.T) {
  83. svcCtx := newAdminLimitSvcCtx(t, 1)
  84. username := "h1_unk_" + testutil.UniqueId()
  85. req := &types.AdminLoginReq{
  86. Username: username,
  87. Password: "bad",
  88. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  89. }
  90. ctx := context.Background()
  91. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  92. require.Error(t, err)
  93. var ce *response.CodeError
  94. require.True(t, errors.As(err, &ce))
  95. assert.Equal(t, 401, ce.Code())
  96. _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  97. require.Error(t, err)
  98. require.True(t, errors.As(err, &ce))
  99. assert.Equal(t, 429, ce.Code(),
  100. "无 clientIP 时应该退化到 'unknown' 桶继续限流,严禁直接绕过")
  101. }
  102. // TC-0837: managementKey 错误路径不消耗 username quota(Take 顺序冻结)。
  103. func TestAdminLogin_H1_BadManagementKey_DoesNotConsumeQuota(t *testing.T) {
  104. svcCtx := newAdminLimitSvcCtx(t, 1)
  105. username := "h1_mk_" + testutil.UniqueId()
  106. ctx := middleware.WithClientIP(context.Background(), "172.16.0.9")
  107. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{
  108. Username: username,
  109. Password: "whatever",
  110. ManagementKey: "WRONG-KEY",
  111. })
  112. require.Error(t, err)
  113. var ce *response.CodeError
  114. require.True(t, errors.As(err, &ce))
  115. assert.Equal(t, 401, ce.Code())
  116. assert.Equal(t, "managementKey无效", ce.Error())
  117. _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{
  118. Username: username,
  119. Password: "whatever",
  120. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  121. })
  122. require.Error(t, err)
  123. require.True(t, errors.As(err, &ce))
  124. assert.Equal(t, 401, ce.Code(),
  125. "H-1 顺序:managementKey 错误应在 Take 之前 return,不应消耗 per-IP+user 配额")
  126. }