loginService_enum_audit_test.go 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "perms-system-server/internal/testutil"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/require"
  9. "github.com/zeromicro/go-zero/core/limit"
  10. "github.com/zeromicro/go-zero/core/stores/redis"
  11. )
  12. // TC-0751: M-C 修复回归 —— 对不存在的用户名也执行 dummy bcrypt 比对,
  13. // 响应文案与"存在用户但密码错"一致,避免用户名枚举。
  14. func TestValidateProductLogin_UnknownUserSameError(t *testing.T) {
  15. ctx := context.Background()
  16. svcCtx := newTestSvcCtx()
  17. username := "enum_unknown_" + testutil.UniqueId()
  18. _, err := ValidateProductLogin(ctx, svcCtx, username, "random-pw", "test_product", "127.0.0.1")
  19. require.Error(t, err)
  20. var le *LoginError
  21. require.True(t, errors.As(err, &le))
  22. assert.Equal(t, 401, le.Code)
  23. assert.Equal(t, "用户名或密码错误", le.Message,
  24. "M-C:不存在用户名不得暴露差异化文案")
  25. }
  26. // TC-0752: M-C 修复回归 —— 存在用户名但密码错,返回相同文案相同 code,供与 TC-0751 做对照。
  27. func TestValidateProductLogin_KnownUserWrongPwd(t *testing.T) {
  28. ctx := context.Background()
  29. svcCtx := newTestSvcCtx()
  30. username := "enum_known_" + testutil.UniqueId()
  31. userId, cleanUser := insertRefreshTestUser(t, ctx, username, "RightPass123", 1, 2)
  32. t.Cleanup(cleanUser)
  33. _ = userId
  34. _, err := ValidateProductLogin(ctx, svcCtx, username, "wrong-pw", "test_product", "127.0.0.1")
  35. require.Error(t, err)
  36. var le *LoginError
  37. require.True(t, errors.As(err, &le))
  38. assert.Equal(t, 401, le.Code, "M-C:Code 必须与未知用户完全一致")
  39. assert.Equal(t, "用户名或密码错误", le.Message, "M-C:文案必须与未知用户完全一致")
  40. }
  41. // TC-0753: M-C 修复回归 —— UsernameLoginLimit 的 key 必须按 ip:username 构造。
  42. // 同一 username 不同 IP 的配额互不共用,防止攻击者"用任意 IP 打爆某账号"导致账号 DoS。
  43. func TestValidateProductLogin_RateLimitKeyedByIPAndUsername(t *testing.T) {
  44. ctx := context.Background()
  45. svcCtx := newTestSvcCtx()
  46. // 使用独立的 quota=1 limiter
  47. cfg := testutil.GetTestConfig()
  48. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  49. svcCtx.UsernameLoginLimit = limit.NewPeriodLimit(300, 1, rds,
  50. cfg.CacheRedis.KeyPrefix+":rl:userlogin:ut:"+testutil.UniqueId())
  51. username := "enum_rl_" + testutil.UniqueId()
  52. // IP-A 第 1 次:"用户名或密码错误"
  53. _, err := ValidateProductLogin(ctx, svcCtx, username, "x", "test_product", "1.1.1.1")
  54. require.Error(t, err)
  55. var le *LoginError
  56. require.True(t, errors.As(err, &le))
  57. assert.Equal(t, 401, le.Code)
  58. // IP-A 第 2 次:超限 429
  59. _, err = ValidateProductLogin(ctx, svcCtx, username, "x", "test_product", "1.1.1.1")
  60. require.Error(t, err)
  61. require.True(t, errors.As(err, &le))
  62. assert.Equal(t, 429, le.Code, "M-C:同 IP 同 username 第 2 次必须触发 429")
  63. // IP-B 第 1 次:独立桶,仍应走到密码校验(不是 429)
  64. _, err = ValidateProductLogin(ctx, svcCtx, username, "x", "test_product", "2.2.2.2")
  65. require.Error(t, err)
  66. require.True(t, errors.As(err, &le))
  67. assert.Equal(t, 401, le.Code,
  68. "M-C:不同 IP 的同 username 必须走独立限流桶(不是 429)")
  69. }