adminLoginLogic_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. "github.com/zeromicro/go-zero/core/limit"
  8. "github.com/zeromicro/go-zero/core/stores/redis"
  9. "perms-system-server/internal/middleware"
  10. "perms-system-server/internal/response"
  11. "perms-system-server/internal/svc"
  12. "perms-system-server/internal/testutil"
  13. "perms-system-server/internal/types"
  14. "testing"
  15. "time"
  16. )
  17. func TestAdminLogin_SuperAdmin(t *testing.T) {
  18. ctx := context.Background()
  19. svcCtx := newTestSvcCtx()
  20. username := testutil.UniqueId()
  21. password := "TestPass123"
  22. _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 1)
  23. t.Cleanup(cleanUser)
  24. logic := NewAdminLoginLogic(ctx, svcCtx)
  25. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  26. Username: username,
  27. Password: password,
  28. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  29. })
  30. require.NoError(t, err)
  31. require.NotNil(t, resp)
  32. assert.NotEmpty(t, resp.AccessToken)
  33. assert.NotEmpty(t, resp.RefreshToken)
  34. assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳")
  35. assert.Equal(t, username, resp.UserInfo.Username)
  36. assert.Equal(t, int64(1), resp.UserInfo.IsSuperAdmin)
  37. // 审计 L-R18-3:loadPerms 出口 Perms 恒为非 nil []string{};管理后台无 productCode 不加载权限,
  38. // 但 Perms 不再是 nil,而是 []string{}(JSON 序列化为 [],不再为 null)。
  39. assert.NotNil(t, resp.UserInfo.Perms, "Perms 必须为非 nil 空 slice([]string{}),而非 nil")
  40. assert.Empty(t, resp.UserInfo.Perms, "管理后台不传 productCode,不应加载任何权限列表")
  41. assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType)
  42. }
  43. // TC-0016: 普通用户被拒绝(1修复: 仅超管可通过管理后台登录)
  44. func TestAdminLogin_NormalUserRejected(t *testing.T) {
  45. ctx := context.Background()
  46. svcCtx := newTestSvcCtx()
  47. username := testutil.UniqueId()
  48. password := "TestPass123"
  49. _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 2)
  50. t.Cleanup(cleanUser)
  51. logic := NewAdminLoginLogic(ctx, svcCtx)
  52. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  53. Username: username,
  54. Password: password,
  55. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  56. })
  57. require.Nil(t, resp)
  58. require.Error(t, err)
  59. var codeErr *response.CodeError
  60. require.True(t, errors.As(err, &codeErr))
  61. assert.Equal(t, 401, codeErr.Code())
  62. assert.Equal(t, "用户名或密码错误", codeErr.Error())
  63. }
  64. // TC-0017: managementKey无效
  65. func TestAdminLogin_InvalidManagementKey(t *testing.T) {
  66. ctx := context.Background()
  67. svcCtx := newTestSvcCtx()
  68. logic := NewAdminLoginLogic(ctx, svcCtx)
  69. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  70. Username: "anyone",
  71. Password: "pass",
  72. ManagementKey: "wrong-key",
  73. })
  74. require.Nil(t, resp)
  75. require.Error(t, err)
  76. var codeErr *response.CodeError
  77. require.True(t, errors.As(err, &codeErr))
  78. assert.Equal(t, 401, codeErr.Code())
  79. assert.Equal(t, "managementKey无效", codeErr.Error())
  80. }
  81. // TC-0018: managementKey为空
  82. func TestAdminLogin_EmptyManagementKey(t *testing.T) {
  83. ctx := context.Background()
  84. svcCtx := newTestSvcCtx()
  85. logic := NewAdminLoginLogic(ctx, svcCtx)
  86. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  87. Username: "anyone",
  88. Password: "pass",
  89. ManagementKey: "",
  90. })
  91. require.Nil(t, resp)
  92. require.Error(t, err)
  93. var codeErr *response.CodeError
  94. require.True(t, errors.As(err, &codeErr))
  95. assert.Equal(t, 401, codeErr.Code())
  96. assert.Equal(t, "managementKey无效", codeErr.Error())
  97. }
  98. // TC-0019: 用户不存在
  99. func TestAdminLogin_UserNotFound(t *testing.T) {
  100. ctx := context.Background()
  101. svcCtx := newTestSvcCtx()
  102. logic := NewAdminLoginLogic(ctx, svcCtx)
  103. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  104. Username: "nonexistent_" + testutil.UniqueId(),
  105. Password: "whatever",
  106. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  107. })
  108. require.Nil(t, resp)
  109. require.Error(t, err)
  110. var codeErr *response.CodeError
  111. require.True(t, errors.As(err, &codeErr))
  112. assert.Equal(t, 401, codeErr.Code())
  113. assert.Equal(t, "用户名或密码错误", codeErr.Error())
  114. }
  115. // TC-0020: 密码错误
  116. func TestAdminLogin_WrongPassword(t *testing.T) {
  117. ctx := context.Background()
  118. svcCtx := newTestSvcCtx()
  119. username := testutil.UniqueId()
  120. _, cleanUser := insertTestUser(t, ctx, svcCtx, username, "CorrectPass", 1, 2)
  121. t.Cleanup(cleanUser)
  122. logic := NewAdminLoginLogic(ctx, svcCtx)
  123. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  124. Username: username,
  125. Password: "WrongPass",
  126. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  127. })
  128. require.Nil(t, resp)
  129. require.Error(t, err)
  130. var codeErr *response.CodeError
  131. require.True(t, errors.As(err, &codeErr))
  132. assert.Equal(t, 401, codeErr.Code())
  133. assert.Equal(t, "用户名或密码错误", codeErr.Error())
  134. }
  135. // TC-0021: 账号冻结
  136. func TestAdminLogin_AccountFrozen(t *testing.T) {
  137. ctx := context.Background()
  138. svcCtx := newTestSvcCtx()
  139. username := testutil.UniqueId()
  140. password := "TestPass123"
  141. _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 2, 2)
  142. t.Cleanup(cleanUser)
  143. logic := NewAdminLoginLogic(ctx, svcCtx)
  144. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  145. Username: username,
  146. Password: password,
  147. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  148. })
  149. require.Nil(t, resp)
  150. require.Error(t, err)
  151. var codeErr *response.CodeError
  152. require.True(t, errors.As(err, &codeErr))
  153. assert.Equal(t, 401, codeErr.Code())
  154. assert.Equal(t, "用户名或密码错误", codeErr.Error())
  155. }
  156. // TC-0022: 不带productCode时token无权限(perms为空)
  157. func TestAdminLogin_NoPermsWithoutProductCode(t *testing.T) {
  158. ctx := context.Background()
  159. svcCtx := newTestSvcCtx()
  160. username := testutil.UniqueId()
  161. password := "TestPass123"
  162. _, cleanUser := insertTestUser(t, ctx, svcCtx, username, password, 1, 1)
  163. t.Cleanup(cleanUser)
  164. logic := NewAdminLoginLogic(ctx, svcCtx)
  165. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  166. Username: username,
  167. Password: password,
  168. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  169. })
  170. require.NoError(t, err)
  171. require.NotNil(t, resp)
  172. assert.NotNil(t, resp.UserInfo.Perms, "Perms 必须为非 nil 的空 slice([]string{})")
  173. assert.Empty(t, resp.UserInfo.Perms, "管理后台不传productCode,不应加载权限列表")
  174. assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType, "超管即使不传productCode也会被标记SUPER_ADMIN")
  175. }
  176. // TC-0025: adminLogin 用户名级别限流(修复验证)
  177. func TestAdminLogin_UsernameRateLimit(t *testing.T) {
  178. ctx := context.Background()
  179. svcCtx := newTestSvcCtx()
  180. require.NotNil(t, svcCtx.UsernameLoginLimit, "UsernameLoginLimit 应被配置")
  181. username := "rl_" + testutil.UniqueId()
  182. logic := NewAdminLoginLogic(ctx, svcCtx)
  183. var last error
  184. for i := 0; i < 11; i++ {
  185. _, last = logic.AdminLogin(&types.AdminLoginReq{
  186. Username: username,
  187. Password: "wrong_pass",
  188. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  189. })
  190. require.Error(t, last)
  191. }
  192. var ce *response.CodeError
  193. require.True(t, errors.As(last, &ce))
  194. assert.Equal(t, 429, ce.Code(), "第11次应被用户名级限流")
  195. }
  196. // TC-0024: SQL注入username
  197. func TestAdminLogin_SQLInjection(t *testing.T) {
  198. ctx := context.Background()
  199. svcCtx := newTestSvcCtx()
  200. logic := NewAdminLoginLogic(ctx, svcCtx)
  201. resp, err := logic.AdminLogin(&types.AdminLoginReq{
  202. Username: "' OR 1=1 --",
  203. Password: "anything",
  204. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  205. })
  206. require.Nil(t, resp)
  207. require.Error(t, err)
  208. var codeErr *response.CodeError
  209. require.True(t, errors.As(err, &codeErr))
  210. assert.Equal(t, 401, codeErr.Code())
  211. assert.Equal(t, "用户名或密码错误", codeErr.Error())
  212. }
  213. func newAdminLimitSvcCtx(t *testing.T, quota int) *svc.ServiceContext {
  214. t.Helper()
  215. cfg := testutil.GetTestConfig()
  216. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  217. svcCtx := newTestSvcCtx()
  218. svcCtx.UsernameLoginLimit = limit.NewPeriodLimit(300, quota, rds,
  219. cfg.CacheRedis.KeyPrefix+":rl:adminlogin:ut:"+testutil.UniqueId())
  220. return svcCtx
  221. }
  222. // TC-0834: 同 IP + 同 username 超过 quota 必须 429,文案为新版本。
  223. func TestAdminLogin_H1_SameIPSameUsername_OverQuota429(t *testing.T) {
  224. svcCtx := newAdminLimitSvcCtx(t, 1)
  225. username := "h1_user_" + testutil.UniqueId()
  226. ctx := middleware.WithClientIP(context.Background(), "1.2.3.4")
  227. req := &types.AdminLoginReq{
  228. Username: username,
  229. Password: "bad",
  230. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  231. }
  232. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  233. require.Error(t, err)
  234. var ce *response.CodeError
  235. require.True(t, errors.As(err, &ce))
  236. assert.Equal(t, 401, ce.Code(), "首次调用应被限流放行并进入业务层,得到 401")
  237. _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  238. require.Error(t, err)
  239. require.True(t, errors.As(err, &ce))
  240. assert.Equal(t, 429, ce.Code(), "同 IP+同 username 第二次必须 429")
  241. assert.Equal(t, "登录尝试过于频繁,请5分钟后再试", ce.Error())
  242. }
  243. // TC-0835: 同 username 换远端 IP 不得继承配额。
  244. func TestAdminLogin_H1_DifferentIPSameUsername_IndependentBucket(t *testing.T) {
  245. svcCtx := newAdminLimitSvcCtx(t, 1)
  246. username := "h1_iso_" + testutil.UniqueId()
  247. req := &types.AdminLoginReq{
  248. Username: username,
  249. Password: "bad",
  250. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  251. }
  252. ctxA := middleware.WithClientIP(context.Background(), "10.0.0.1")
  253. _, err := NewAdminLoginLogic(ctxA, svcCtx).AdminLogin(req)
  254. require.Error(t, err)
  255. var ce *response.CodeError
  256. require.True(t, errors.As(err, &ce))
  257. assert.Equal(t, 401, ce.Code())
  258. _, err = NewAdminLoginLogic(ctxA, svcCtx).AdminLogin(req)
  259. require.Error(t, err)
  260. require.True(t, errors.As(err, &ce))
  261. assert.Equal(t, 429, ce.Code(), "IP-A 配额已满")
  262. ctxB := middleware.WithClientIP(context.Background(), "10.0.0.2")
  263. _, err = NewAdminLoginLogic(ctxB, svcCtx).AdminLogin(req)
  264. require.Error(t, err)
  265. require.True(t, errors.As(err, &ce))
  266. assert.Equal(t, 401, ce.Code(),
  267. "换远端 IP 必须命中独立限流桶,不能被同 username 的旧计数拖连")
  268. }
  269. // TC-0836: ctx 里无 clientIP —— 退化为 "unknown" 共享桶,仍能限流,不得绕过。
  270. func TestAdminLogin_H1_MissingClientIP_FallbackBucket(t *testing.T) {
  271. svcCtx := newAdminLimitSvcCtx(t, 1)
  272. username := "h1_unk_" + testutil.UniqueId()
  273. req := &types.AdminLoginReq{
  274. Username: username,
  275. Password: "bad",
  276. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  277. }
  278. ctx := context.Background()
  279. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  280. require.Error(t, err)
  281. var ce *response.CodeError
  282. require.True(t, errors.As(err, &ce))
  283. assert.Equal(t, 401, ce.Code())
  284. _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(req)
  285. require.Error(t, err)
  286. require.True(t, errors.As(err, &ce))
  287. assert.Equal(t, 429, ce.Code(),
  288. "无 clientIP 时应该退化到 'unknown' 桶继续限流,严禁直接绕过")
  289. }
  290. // TC-0837: managementKey 错误路径不消耗 username quota(Take 顺序冻结)。
  291. func TestAdminLogin_H1_BadManagementKey_DoesNotConsumeQuota(t *testing.T) {
  292. svcCtx := newAdminLimitSvcCtx(t, 1)
  293. username := "h1_mk_" + testutil.UniqueId()
  294. ctx := middleware.WithClientIP(context.Background(), "172.16.0.9")
  295. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{
  296. Username: username,
  297. Password: "whatever",
  298. ManagementKey: "WRONG-KEY",
  299. })
  300. require.Error(t, err)
  301. var ce *response.CodeError
  302. require.True(t, errors.As(err, &ce))
  303. assert.Equal(t, 401, ce.Code())
  304. assert.Equal(t, "managementKey无效", ce.Error())
  305. _, err = NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{
  306. Username: username,
  307. Password: "whatever",
  308. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  309. })
  310. require.Error(t, err)
  311. require.True(t, errors.As(err, &ce))
  312. assert.Equal(t, 401, ce.Code(),
  313. "managementKey 错误应在 Take 之前 return,不应消耗 per-IP+user 配额")
  314. }
  315. func TestAdminLogin_LN3_NonSuperAdminWrongPassword_IndistinguishableFromAbsent(t *testing.T) {
  316. ctx := context.Background()
  317. svcCtx := newTestSvcCtx()
  318. svcCtx.UsernameLoginLimit = nil
  319. username := "ln3_nonsa_" + testutil.UniqueId()
  320. // status=1(启用),isSuperAdmin=2(普通用户)
  321. _, clean := insertTestUser(t, ctx, svcCtx, username, "RightPass123", 1, 2)
  322. t.Cleanup(clean)
  323. logic := NewAdminLoginLogic(ctx, svcCtx)
  324. // (B) 用户存在但非超管 —— 走 新增的 dummy bcrypt 分支
  325. _, errExisting := logic.AdminLogin(&types.AdminLoginReq{
  326. Username: username,
  327. Password: "WrongPass",
  328. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  329. })
  330. require.Error(t, errExisting)
  331. var ceB *response.CodeError
  332. require.True(t, errors.As(errExisting, &ceB))
  333. // (A) 用户不存在 —— 原有 dummy bcrypt 分支
  334. _, errAbsent := logic.AdminLogin(&types.AdminLoginReq{
  335. Username: "ln3_absent_" + testutil.UniqueId(),
  336. Password: "WhateverPass",
  337. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  338. })
  339. require.Error(t, errAbsent)
  340. var ceA *response.CodeError
  341. require.True(t, errors.As(errAbsent, &ceA))
  342. assert.Equal(t, ceA.Code(), ceB.Code(),
  343. "'非超管 + 错误密码' 与 '用户不存在' 必须返回相同 code")
  344. assert.Equal(t, ceA.Error(), ceB.Error(),
  345. "'非超管 + 错误密码' 与 '用户不存在' 必须返回相同 body")
  346. assert.Equal(t, "用户名或密码错误", ceB.Error())
  347. }
  348. // TC-1009: 非超管账号 + 任意密码(包括正确密码)都必须 401,且仍触发一次 bcrypt,
  349. // 保证即使攻击者命中密码,也不得通过 response 推断该账号是"存在的普通用户"。
  350. func TestAdminLogin_LN3_NonSuperAdminCorrectPassword_Still401(t *testing.T) {
  351. ctx := context.Background()
  352. svcCtx := newTestSvcCtx()
  353. svcCtx.UsernameLoginLimit = nil
  354. username := "ln3_cp_" + testutil.UniqueId()
  355. password := "KnownPass123"
  356. _, clean := insertTestUser(t, ctx, svcCtx, username, password, 1, 2)
  357. t.Cleanup(clean)
  358. _, err := NewAdminLoginLogic(ctx, svcCtx).AdminLogin(&types.AdminLoginReq{
  359. Username: username,
  360. Password: password,
  361. ManagementKey: svcCtx.Config.Auth.ManagementKey,
  362. })
  363. require.Error(t, err)
  364. var ce *response.CodeError
  365. require.True(t, errors.As(err, &ce))
  366. assert.Equal(t, 401, ce.Code(),
  367. "非超管走 AdminLogin 一律 401,即使密码正确也不得披露账号存在性")
  368. assert.Equal(t, "用户名或密码错误", ce.Error())
  369. }
  370. // TC-1010: 时序等齐 —— "非超管 + 错密码" 必须与 "用户不存在" 同阶(两者都走一次 dummyBcryptHash)。
  371. //
  372. // 注意:测试环境里通过 testutil.HashPassword 生成真实用户的 bcrypt 哈希时使用了 MinCost(cost=4)
  373. // 以提速;而生产代码里的 dummyBcryptHash 固定用 DefaultCost(cost=10)。因此"超管 + 错密码"走
  374. // 真 bcrypt(cost=4) 会显著快于两条 dummy 分支,这里无法把 SA+wrong 的耗时纳入对比。
  375. // 本 TC 只对比两条 dummy 分支——它们共用同一份 dummyBcryptHash,理应严格齐平(2× 以内)。
  376. // 若非超管分支被回退到"不走 dummy bcrypt",dNonSa 会突然下降一个数量级,ratio 会突破 5× 触发 FAIL。
  377. func TestAdminLogin_LN3_DummyBcryptBranches_TimingEqualized(t *testing.T) {
  378. if testing.Short() {
  379. t.Skip("timing-sensitive test skipped under -short")
  380. }
  381. ctx := context.Background()
  382. svcCtx := newTestSvcCtx()
  383. svcCtx.UsernameLoginLimit = nil
  384. normalUser := "ln3_t_nm_" + testutil.UniqueId()
  385. _, cleanNm := insertTestUser(t, ctx, svcCtx, normalUser, "RealNormalPass123", 1, 2)
  386. t.Cleanup(cleanNm)
  387. logic := NewAdminLoginLogic(ctx, svcCtx)
  388. mk := svcCtx.Config.Auth.ManagementKey
  389. measure := func(username, password string) time.Duration {
  390. _, _ = logic.AdminLogin(&types.AdminLoginReq{Username: username, Password: password, ManagementKey: mk})
  391. const N = 3
  392. var total time.Duration
  393. for i := 0; i < N; i++ {
  394. start := time.Now()
  395. _, _ = logic.AdminLogin(&types.AdminLoginReq{Username: username, Password: password, ManagementKey: mk})
  396. total += time.Since(start)
  397. }
  398. return total / N
  399. }
  400. dAbsent := measure("ln3_absent_"+testutil.UniqueId(), "xx")
  401. dNonSa := measure(normalUser, "WrongPass")
  402. t.Logf("dummy bcrypt timing: absent=%v nonSa=%v", dAbsent, dNonSa)
  403. ratio := func(a, b time.Duration) float64 {
  404. if b <= 0 {
  405. return 0
  406. }
  407. if a > b {
  408. return float64(a) / float64(b)
  409. }
  410. return float64(b) / float64(a)
  411. }
  412. const tol = 3.0 // CI 抖动容忍
  413. assert.Less(t, ratio(dNonSa, dAbsent), tol,
  414. "'非超管 + 错密码' 必须与 '用户不存在' 耗时同阶;若 >3× 说明 L-N3 被回退(非超管分支没走 dummy bcrypt)")
  415. }