| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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:<clientIP>:<username>` 双维。
- //
- // H-1 攻击:攻击者只靠已知或枚举出的超管用户名 `admin_<productCode>`,从任意远端 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 配额")
- }
|