refreshTokenLogic_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. package pub
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. "github.com/zeromicro/go-zero/core/limit"
  9. "github.com/zeromicro/go-zero/core/stores/redis"
  10. authHelper "perms-system-server/internal/logic/auth"
  11. "perms-system-server/internal/middleware"
  12. permModel "perms-system-server/internal/model/perm"
  13. productmemberModel "perms-system-server/internal/model/productmember"
  14. userModel "perms-system-server/internal/model/user"
  15. "perms-system-server/internal/response"
  16. "perms-system-server/internal/testutil"
  17. "perms-system-server/internal/types"
  18. "sync"
  19. "sync/atomic"
  20. "testing"
  21. "time"
  22. )
  23. func insertRefreshTestUser(t *testing.T, ctx context.Context, username, password string, status, isSuperAdmin int64) (int64, func()) {
  24. t.Helper()
  25. svcCtx := newTestSvcCtx()
  26. conn := testutil.GetTestSqlConn()
  27. now := time.Now().Unix()
  28. hashed := testutil.HashPassword(password)
  29. res, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  30. Username: username,
  31. Password: hashed,
  32. Nickname: username,
  33. Avatar: sql.NullString{},
  34. Email: username + "@test.com",
  35. Phone: "13800000000",
  36. Remark: "",
  37. DeptId: 0,
  38. IsSuperAdmin: isSuperAdmin,
  39. MustChangePassword: 2,
  40. Status: status,
  41. CreateTime: now,
  42. UpdateTime: now,
  43. })
  44. require.NoError(t, err)
  45. id, _ := res.LastInsertId()
  46. cleanup := func() {
  47. testutil.CleanTable(ctx, conn, "`sys_user`", id)
  48. }
  49. return id, cleanup
  50. }
  51. // TC-0026: 正常刷新(refreshToken从header获取,原样返回不重新生成)
  52. func TestRefreshToken_Normal(t *testing.T) {
  53. ctx := context.Background()
  54. svcCtx := newTestSvcCtx()
  55. username := testutil.UniqueId()
  56. password := "TestPass123"
  57. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  58. t.Cleanup(cleanUser)
  59. refreshToken, err := authHelper.GenerateRefreshToken(
  60. svcCtx.Config.Auth.RefreshSecret,
  61. svcCtx.Config.Auth.RefreshExpire,
  62. userId, "", 0,
  63. )
  64. require.NoError(t, err)
  65. logic := NewRefreshTokenLogic(ctx, svcCtx)
  66. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  67. Authorization: "Bearer " + refreshToken,
  68. })
  69. require.NoError(t, err)
  70. require.NotNil(t, resp)
  71. assert.NotEmpty(t, resp.AccessToken)
  72. assert.NotEmpty(t, resp.RefreshToken, "应返回新的refreshToken")
  73. assert.NotEqual(t, resp.AccessToken, resp.RefreshToken, "accessToken和refreshToken应不同")
  74. assert.True(t, resp.Expires > time.Now().Unix(), "expires应为未来的unix时间戳")
  75. assert.Equal(t, userId, resp.UserInfo.UserId)
  76. assert.Equal(t, username, resp.UserInfo.Username)
  77. }
  78. // TC-0027: 不带productCode(回退)
  79. func TestRefreshToken_FallbackToClaimsProductCode(t *testing.T) {
  80. ctx := context.Background()
  81. svcCtx := newTestSvcCtx()
  82. conn := testutil.GetTestSqlConn()
  83. username := testutil.UniqueId()
  84. password := "TestPass123"
  85. pc := testutil.UniqueId()
  86. now := time.Now().Unix()
  87. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  88. t.Cleanup(cleanUser)
  89. _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret")
  90. t.Cleanup(cleanProduct)
  91. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  92. ProductCode: pc, UserId: userId, MemberType: "ADMIN", Status: 1, CreateTime: now, UpdateTime: now,
  93. })
  94. require.NoError(t, err)
  95. pmId, _ := pmRes.LastInsertId()
  96. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId) })
  97. permCode := testutil.UniqueId()
  98. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  99. ProductCode: pc, Name: "refresh_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now,
  100. })
  101. require.NoError(t, err)
  102. permId, _ := permRes.LastInsertId()
  103. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  104. refreshToken, err := authHelper.GenerateRefreshToken(
  105. svcCtx.Config.Auth.RefreshSecret,
  106. svcCtx.Config.Auth.RefreshExpire,
  107. userId, pc, 0,
  108. )
  109. require.NoError(t, err)
  110. logic := NewRefreshTokenLogic(ctx, svcCtx)
  111. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  112. Authorization: "Bearer " + refreshToken,
  113. })
  114. require.NoError(t, err)
  115. require.NotNil(t, resp)
  116. assert.Equal(t, "ADMIN", resp.UserInfo.MemberType)
  117. assert.Contains(t, resp.UserInfo.Perms, permCode)
  118. }
  119. // TC-0028: token无效
  120. func TestRefreshToken_InvalidToken(t *testing.T) {
  121. ctx := context.Background()
  122. svcCtx := newTestSvcCtx()
  123. logic := NewRefreshTokenLogic(ctx, svcCtx)
  124. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  125. Authorization: "Bearer invalid.token.string",
  126. })
  127. require.Nil(t, resp)
  128. require.Error(t, err)
  129. var codeErr *response.CodeError
  130. require.True(t, errors.As(err, &codeErr))
  131. assert.Equal(t, 401, codeErr.Code())
  132. assert.Equal(t, "refreshToken无效或已过期", codeErr.Error())
  133. }
  134. // TC-0029: 用户已被删除 —— 修复后必须区分"不存在"(401) 与"冻结"(403)。
  135. //
  136. // 修复前:Loader 对不存在用户返回空壳 UserDetails(Status=0),RefreshToken 走到"账号已被冻结"分支 (403),
  137. //
  138. // 将"用户不存在"与"账号冻结"两个语义混淆,监控告警与运维处置策略无法区分。
  139. //
  140. // 修复后:Loader 返回 (ud, nil) 且 ud.Username == "",RefreshToken 显式回 401 "用户不存在或已被删除"。
  141. //
  142. // 这样客户端/前端才能走"注销本地会话 + 返回登录页"的终态流程,而不是提示"账号已冻结请联系管理员"。
  143. func TestRefreshToken_UserDeleted(t *testing.T) {
  144. ctx := context.Background()
  145. svcCtx := newTestSvcCtx()
  146. nonExistentUserId := int64(999999999)
  147. refreshToken, err := authHelper.GenerateRefreshToken(
  148. svcCtx.Config.Auth.RefreshSecret,
  149. svcCtx.Config.Auth.RefreshExpire,
  150. nonExistentUserId, "", 0,
  151. )
  152. require.NoError(t, err)
  153. logic := NewRefreshTokenLogic(ctx, svcCtx)
  154. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  155. Authorization: "Bearer " + refreshToken,
  156. })
  157. require.Nil(t, resp)
  158. require.Error(t, err)
  159. var codeErr *response.CodeError
  160. require.True(t, errors.As(err, &codeErr))
  161. assert.Equal(t, 401, codeErr.Code(), "用户不存在必须走 401,不得与冻结态 (403) 混淆")
  162. assert.Equal(t, "用户不存在或已被删除", codeErr.Error())
  163. }
  164. // TC-0030: 账号冻结
  165. func TestRefreshToken_AccountFrozen(t *testing.T) {
  166. ctx := context.Background()
  167. svcCtx := newTestSvcCtx()
  168. username := testutil.UniqueId()
  169. password := "TestPass123"
  170. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 2, 2)
  171. t.Cleanup(cleanUser)
  172. refreshToken, err := authHelper.GenerateRefreshToken(
  173. svcCtx.Config.Auth.RefreshSecret,
  174. svcCtx.Config.Auth.RefreshExpire,
  175. userId, "", 0,
  176. )
  177. require.NoError(t, err)
  178. logic := NewRefreshTokenLogic(ctx, svcCtx)
  179. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  180. Authorization: "Bearer " + refreshToken,
  181. })
  182. require.Nil(t, resp)
  183. require.Error(t, err)
  184. var codeErr *response.CodeError
  185. require.True(t, errors.As(err, &codeErr))
  186. assert.Equal(t, 403, codeErr.Code())
  187. assert.Equal(t, "账号已被冻结", codeErr.Error())
  188. }
  189. // TC-0032: 尝试切换产品被拒绝
  190. func TestRefreshToken_ProductCodeSwitchRejected(t *testing.T) {
  191. ctx := context.Background()
  192. svcCtx := newTestSvcCtx()
  193. username := testutil.UniqueId()
  194. password := "TestPass123"
  195. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  196. t.Cleanup(cleanUser)
  197. refreshToken, err := authHelper.GenerateRefreshToken(
  198. svcCtx.Config.Auth.RefreshSecret,
  199. svcCtx.Config.Auth.RefreshExpire,
  200. userId, "product_a", 0,
  201. )
  202. require.NoError(t, err)
  203. logic := NewRefreshTokenLogic(ctx, svcCtx)
  204. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  205. Authorization: "Bearer " + refreshToken,
  206. ProductCode: "product_b",
  207. })
  208. require.Nil(t, resp)
  209. require.Error(t, err)
  210. var codeErr *response.CodeError
  211. require.True(t, errors.As(err, &codeErr))
  212. assert.Equal(t, 400, codeErr.Code())
  213. assert.Equal(t, "刷新令牌不允许切换产品", codeErr.Error())
  214. }
  215. // TC-0033: TokenVersion不匹配时拒绝刷新
  216. func TestRefreshToken_TokenVersionMismatch(t *testing.T) {
  217. ctx := context.Background()
  218. svcCtx := newTestSvcCtx()
  219. username := testutil.UniqueId()
  220. password := "TestPass123"
  221. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  222. t.Cleanup(cleanUser)
  223. refreshToken, err := authHelper.GenerateRefreshToken(
  224. svcCtx.Config.Auth.RefreshSecret,
  225. svcCtx.Config.Auth.RefreshExpire,
  226. userId, "", 999,
  227. )
  228. require.NoError(t, err)
  229. logic := NewRefreshTokenLogic(ctx, svcCtx)
  230. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  231. Authorization: "Bearer " + refreshToken,
  232. })
  233. require.Nil(t, resp)
  234. require.Error(t, err)
  235. var codeErr *response.CodeError
  236. require.True(t, errors.As(err, &codeErr))
  237. assert.Equal(t, 401, codeErr.Code())
  238. assert.Equal(t, "登录状态已失效,请重新登录", codeErr.Error())
  239. }
  240. // TC-0034: 使用accessToken作为refreshToken被拒绝
  241. func TestRefreshToken_AccessTokenRejected(t *testing.T) {
  242. ctx := context.Background()
  243. svcCtx := newTestSvcCtx()
  244. username := testutil.UniqueId()
  245. password := "TestPass123"
  246. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  247. t.Cleanup(cleanUser)
  248. accessToken, err := authHelper.GenerateAccessToken(
  249. svcCtx.Config.Auth.RefreshSecret,
  250. svcCtx.Config.Auth.AccessExpire,
  251. userId, username, "", "", 0,
  252. )
  253. require.NoError(t, err)
  254. logic := NewRefreshTokenLogic(ctx, svcCtx)
  255. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  256. Authorization: "Bearer " + accessToken,
  257. })
  258. require.Nil(t, resp)
  259. require.Error(t, err)
  260. var codeErr *response.CodeError
  261. require.True(t, errors.As(err, &codeErr))
  262. assert.Equal(t, 401, codeErr.Code())
  263. assert.Equal(t, "refreshToken无效或已过期", codeErr.Error())
  264. }
  265. // TC-0035: 产品成员已移除时拒绝刷新
  266. func TestRefreshToken_MemberRemovedRejected(t *testing.T) {
  267. ctx := context.Background()
  268. svcCtx := newTestSvcCtx()
  269. conn := testutil.GetTestSqlConn()
  270. username := testutil.UniqueId()
  271. password := "TestPass123"
  272. pc := testutil.UniqueId()
  273. now := time.Now().Unix()
  274. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  275. t.Cleanup(cleanUser)
  276. _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret")
  277. t.Cleanup(cleanProduct)
  278. pmRes, err := svcCtx.SysProductMemberModel.Insert(ctx, &productmemberModel.SysProductMember{
  279. ProductCode: pc, UserId: userId, MemberType: "MEMBER", Status: 1, CreateTime: now, UpdateTime: now,
  280. })
  281. require.NoError(t, err)
  282. pmId, _ := pmRes.LastInsertId()
  283. refreshToken, err := authHelper.GenerateRefreshToken(
  284. svcCtx.Config.Auth.RefreshSecret,
  285. svcCtx.Config.Auth.RefreshExpire,
  286. userId, pc, 0,
  287. )
  288. require.NoError(t, err)
  289. testutil.CleanTable(ctx, conn, "`sys_product_member`", pmId)
  290. logic := NewRefreshTokenLogic(ctx, svcCtx)
  291. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  292. Authorization: "Bearer " + refreshToken,
  293. })
  294. require.Nil(t, resp)
  295. require.Error(t, err)
  296. var codeErr *response.CodeError
  297. require.True(t, errors.As(err, &codeErr))
  298. assert.Equal(t, 403, codeErr.Code())
  299. assert.Equal(t, "您已不是该产品的成员", codeErr.Error())
  300. }
  301. // TC-0031: 超管+productCode(refreshToken原样返回)
  302. func TestRefreshToken_SuperAdminWithProductCode(t *testing.T) {
  303. ctx := context.Background()
  304. svcCtx := newTestSvcCtx()
  305. conn := testutil.GetTestSqlConn()
  306. username := testutil.UniqueId()
  307. password := "TestPass123"
  308. pc := testutil.UniqueId()
  309. now := time.Now().Unix()
  310. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 1)
  311. t.Cleanup(cleanUser)
  312. _, cleanProduct := insertTestProduct(t, ctx, svcCtx, pc, testutil.UniqueId(), "secret")
  313. t.Cleanup(cleanProduct)
  314. permCode := testutil.UniqueId()
  315. permRes, err := svcCtx.SysPermModel.Insert(ctx, &permModel.SysPerm{
  316. ProductCode: pc, Name: "sa_refresh_perm", Code: permCode, Status: 1, CreateTime: now, UpdateTime: now,
  317. })
  318. require.NoError(t, err)
  319. permId, _ := permRes.LastInsertId()
  320. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_perm`", permId) })
  321. refreshToken, err := authHelper.GenerateRefreshToken(
  322. svcCtx.Config.Auth.RefreshSecret,
  323. svcCtx.Config.Auth.RefreshExpire,
  324. userId, pc, 0,
  325. )
  326. require.NoError(t, err)
  327. logic := NewRefreshTokenLogic(ctx, svcCtx)
  328. resp, err := logic.RefreshToken(&types.RefreshTokenReq{
  329. Authorization: "Bearer " + refreshToken,
  330. ProductCode: pc,
  331. })
  332. require.NoError(t, err)
  333. require.NotNil(t, resp)
  334. assert.NotEmpty(t, resp.RefreshToken, "应返回新的refreshToken")
  335. assert.Equal(t, "SUPER_ADMIN", resp.UserInfo.MemberType)
  336. assert.Contains(t, resp.UserInfo.Perms, permCode)
  337. assert.Equal(t, int64(1), resp.UserInfo.IsSuperAdmin)
  338. }
  339. func TestRefreshToken_ConcurrentSameToken_SingleWinner(t *testing.T) {
  340. ctx := context.Background()
  341. svcCtx := newTestSvcCtx()
  342. username := "rt_cas_" + testutil.UniqueId()
  343. userId, cleanUser := insertRefreshTestUser(t, ctx, username, "TestPass123", 1, 2)
  344. t.Cleanup(cleanUser)
  345. // 禁用 TokenOpLimiter,以让本测试的变量只剩"并发 CAS 胜负"。
  346. svcCtx.TokenOpLimiter = nil
  347. rt, err := authHelper.GenerateRefreshToken(
  348. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  349. userId, "", 0,
  350. )
  351. require.NoError(t, err)
  352. // 限制在 6 并发以避免触发 go-zero sqlx breaker(单机 MySQL + breaker 对同批次突发
  353. // 的并发 UPDATE 容易误伤,生产里 refreshToken 也是 per-user 限频 + CAS 双层保护,
  354. // 没机会打成这么高的并发)。CAS "唯一胜出" 的契约在 N=6 时已足以钉死。
  355. const N = 6
  356. var (
  357. wg sync.WaitGroup
  358. okCount int32
  359. authFailCnt int32
  360. otherErr atomic.Value
  361. )
  362. start := make(chan struct{})
  363. for i := 0; i < N; i++ {
  364. wg.Add(1)
  365. go func() {
  366. defer wg.Done()
  367. <-start
  368. resp, e := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
  369. Authorization: "Bearer " + rt,
  370. })
  371. switch {
  372. case e == nil && resp != nil:
  373. atomic.AddInt32(&okCount, 1)
  374. case e != nil:
  375. var ce *response.CodeError
  376. if errors.As(e, &ce) && ce.Code() == 401 &&
  377. ce.Error() == "登录状态已失效,请重新登录" {
  378. atomic.AddInt32(&authFailCnt, 1)
  379. } else {
  380. otherErr.Store(e)
  381. }
  382. }
  383. }()
  384. }
  385. close(start)
  386. wg.Wait()
  387. if v := otherErr.Load(); v != nil {
  388. t.Fatalf("并发 RefreshToken 出现非预期错误:%v", v)
  389. }
  390. assert.Equal(t, int32(1), atomic.LoadInt32(&okCount),
  391. "会话劫持防线:重放同一旧 refreshToken 的 N 个并发请求必须只有 1 个成功")
  392. assert.Equal(t, int32(N-1), atomic.LoadInt32(&authFailCnt),
  393. "其他并发者必须返回 401 '登录状态已失效'")
  394. // DB 必然只递增 1。
  395. u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  396. require.NoError(t, err)
  397. assert.Equal(t, int64(1), u.TokenVersion,
  398. "DB tokenVersion 递增幅度就是 CAS 成功次数 → 只能是 1")
  399. }
  400. func TestRefreshToken_TokenOpLimiter_BlocksBurst(t *testing.T) {
  401. ctx := context.Background()
  402. svcCtx := newTestSvcCtx()
  403. username := "rt_rl_" + testutil.UniqueId()
  404. password := "TestPass123"
  405. userId, cleanUser := insertRefreshTestUser(t, ctx, username, password, 1, 2)
  406. t.Cleanup(cleanUser)
  407. cfg := testutil.GetTestConfig()
  408. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  409. svcCtx.TokenOpLimiter = limit.NewPeriodLimit(60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:refresh:ut:"+testutil.UniqueId())
  410. mkReq := func(tv int64) *types.RefreshTokenReq {
  411. rt, err := authHelper.GenerateRefreshToken(
  412. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  413. userId, "", tv)
  414. require.NoError(t, err)
  415. return &types.RefreshTokenReq{Authorization: "Bearer " + rt}
  416. }
  417. resp1, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(0))
  418. require.NoError(t, err, "首次刷新应放行")
  419. require.NotNil(t, resp1)
  420. // DB tokenVersion 已变为 1,旧 claims.TokenVersion=0 的 refreshToken 已失效,
  421. // 所以第二次必须用新 token;但限流判定在 TokenVersion 校验之**后**、IncrementTokenVersion 之**前**,
  422. // 因此使用新版本号构造的 token 会先通过前置校验,再被 TokenOpLimiter 拦截。
  423. u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  424. require.NoError(t, err)
  425. tvAfterFirst := u.TokenVersion
  426. require.Equal(t, int64(1), tvAfterFirst)
  427. _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(tvAfterFirst))
  428. require.Error(t, err, "超限的第二次刷新必须被 429 拦截")
  429. var ce *response.CodeError
  430. require.True(t, errors.As(err, &ce))
  431. assert.Equal(t, 429, ce.Code())
  432. assert.Contains(t, ce.Error(), "过于频繁")
  433. uAfter, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  434. require.NoError(t, err)
  435. assert.Equal(t, tvAfterFirst, uAfter.TokenVersion,
  436. "被限流的 refresh 请求绝不可递增 tokenVersion")
  437. }
  438. // TC-0742: -B 修复 —— 限流按用户粒度隔离(productCode 无关)。
  439. // 场景:同一用户连续两次带 productCode=空的刷新请求,若限流命中,不会影响其它用户。
  440. func TestRefreshToken_TokenOpLimiter_PerUserIsolated(t *testing.T) {
  441. ctx := context.Background()
  442. svcCtx := newTestSvcCtx()
  443. uaId, cleanA := insertRefreshTestUser(t, ctx, "rt_iso_a_"+testutil.UniqueId(), "TestPass123", 1, 2)
  444. t.Cleanup(cleanA)
  445. ubId, cleanB := insertRefreshTestUser(t, ctx, "rt_iso_b_"+testutil.UniqueId(), "TestPass123", 1, 2)
  446. t.Cleanup(cleanB)
  447. cfg := testutil.GetTestConfig()
  448. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  449. svcCtx.TokenOpLimiter = limit.NewPeriodLimit(60, 1, rds, cfg.CacheRedis.KeyPrefix+":rl:refresh:iso:"+testutil.UniqueId())
  450. mkReq := func(uid, tv int64) *types.RefreshTokenReq {
  451. rt, err := authHelper.GenerateRefreshToken(
  452. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  453. uid, "", tv)
  454. require.NoError(t, err)
  455. return &types.RefreshTokenReq{Authorization: "Bearer " + rt}
  456. }
  457. // A:两次刷新,第 2 次必 429
  458. _, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(uaId, 0))
  459. require.NoError(t, err)
  460. _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(uaId, 1))
  461. require.Error(t, err)
  462. var ce *response.CodeError
  463. require.True(t, errors.As(err, &ce))
  464. require.Equal(t, 429, ce.Code())
  465. // B 应当还能刷新
  466. respB, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(mkReq(ubId, 0))
  467. require.NoError(t, err, "B 用户的限流桶应当独立于 A")
  468. require.NotNil(t, respB)
  469. }
  470. func TestRefreshToken_M3_SuccessEmbedsFreshVersion(t *testing.T) {
  471. ctx := context.Background()
  472. svcCtx := newTestSvcCtx()
  473. svcCtx.TokenOpLimiter = nil
  474. username := "rt_m3_ok_" + testutil.UniqueId()
  475. userId, cleanup := insertRefreshTestUser(t, ctx, username, "TestPass123", 1, 2)
  476. t.Cleanup(cleanup)
  477. rt, err := authHelper.GenerateRefreshToken(
  478. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  479. userId, "", 0,
  480. )
  481. require.NoError(t, err)
  482. resp, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
  483. Authorization: "Bearer " + rt,
  484. })
  485. require.NoError(t, err)
  486. require.NotNil(t, resp)
  487. u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  488. require.NoError(t, err)
  489. assert.Equal(t, int64(1), u.TokenVersion, "正常刷新 DB tokenVersion 必须 +1")
  490. var accessClaims middleware.Claims
  491. _, err = authHelper.ParseWithHMAC(resp.AccessToken, svcCtx.Config.Auth.AccessSecret, &accessClaims)
  492. require.NoError(t, err, "新 accessToken 必须可解析")
  493. assert.Equal(t, u.TokenVersion, accessClaims.TokenVersion,
  494. "新 accessToken.TokenVersion 必须等于 DB 新 tokenVersion;不等说明 CAS/签名顺序错位")
  495. refreshClaims, err := authHelper.ParseRefreshToken(resp.RefreshToken, svcCtx.Config.Auth.RefreshSecret)
  496. require.NoError(t, err, "新 refreshToken 必须可解析")
  497. assert.Equal(t, u.TokenVersion, refreshClaims.TokenVersion,
  498. "新 refreshToken.TokenVersion 必须等于 DB 新 tokenVersion;客户端下一次刷新必须可用")
  499. }
  500. // TC-0984: CAS 失败路径 —— 模拟并发抢先递增后再刷新,DB 不得再次被 +1。
  501. func TestRefreshToken_M3_CASMismatch_DoesNotDoubleAdvance(t *testing.T) {
  502. ctx := context.Background()
  503. svcCtx := newTestSvcCtx()
  504. svcCtx.TokenOpLimiter = nil
  505. username := "rt_m3_cas_" + testutil.UniqueId()
  506. userId, cleanup := insertRefreshTestUser(t, ctx, username, "TestPass123", 1, 2)
  507. t.Cleanup(cleanup)
  508. // 构造旧 refresh token(claims.TokenVersion=0)。
  509. rt, err := authHelper.GenerateRefreshToken(
  510. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  511. userId, "", 0,
  512. )
  513. require.NoError(t, err)
  514. // 模拟"并发赢家已经把 DB tokenVersion 推到 1":直接 CAS 一次。
  515. newVer, err := svcCtx.SysUserModel.IncrementTokenVersionIfMatch(ctx, userId, username, 0)
  516. require.NoError(t, err)
  517. require.Equal(t, int64(1), newVer)
  518. // 清掉用户缓存,确保下一步 Load 能读到 DB 的最新 tokenVersion=1。
  519. svcCtx.UserDetailsLoader.Clean(ctx, userId)
  520. // 现在第二个刷新进来:ud.TokenVersion=1 ≠ claims.TokenVersion=0,
  521. // 会在 logic 第 73 行 "claims.TokenVersion != ud.TokenVersion" 被直接 401 拒,
  522. // 根本到不了 Generate/CAS。DB 不得再次 +1。
  523. resp, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
  524. Authorization: "Bearer " + rt,
  525. })
  526. assert.Nil(t, resp)
  527. require.Error(t, err)
  528. var ce *response.CodeError
  529. require.True(t, errors.As(err, &ce), "必须是 response.CodeError")
  530. assert.Equal(t, 401, ce.Code(), "claims 过期必须是 401")
  531. assert.Equal(t, "登录状态已失效,请重新登录", ce.Error())
  532. // 关键断言:失败分支不得二次推进 tokenVersion。
  533. u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  534. require.NoError(t, err)
  535. assert.Equal(t, int64(1), u.TokenVersion,
  536. "失败分支必须不推进 tokenVersion;若变成 2 说明 CAS 被放在"+
  537. "签名/校验前,已经把用户状态破坏了")
  538. }
  539. // TC-0985: 重放拦截 —— 用第一次刷新拿到的新 refreshToken 再刷一次必须成功;
  540. // 再拿"同一个新 refreshToken"做第三次刷新必须被 401 拦截(tokenVersion 已 +2,claims=+1)。
  541. // 这组断言同时证明 修复之后"预签 token 的版本号 == 最终 DB 版本号"的强契约。
  542. func TestRefreshToken_M3_NewRefreshTokenMatchesDBVersion(t *testing.T) {
  543. ctx := context.Background()
  544. svcCtx := newTestSvcCtx()
  545. svcCtx.TokenOpLimiter = nil
  546. username := "rt_m3_chain_" + testutil.UniqueId()
  547. userId, cleanup := insertRefreshTestUser(t, ctx, username, "TestPass123", 1, 2)
  548. t.Cleanup(cleanup)
  549. first, err := authHelper.GenerateRefreshToken(
  550. svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
  551. userId, "", 0,
  552. )
  553. require.NoError(t, err)
  554. r1, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
  555. Authorization: "Bearer " + first,
  556. })
  557. require.NoError(t, err)
  558. require.NotNil(t, r1)
  559. // 等 loader 缓存被 Clean 后,再用 r1.RefreshToken 续签,理应成功,tokenVersion 从 1 → 2。
  560. r2, err := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
  561. Authorization: "Bearer " + r1.RefreshToken,
  562. })
  563. require.NoError(t, err, "新 refreshToken 必须能顶替旧的继续刷新")
  564. require.NotNil(t, r2)
  565. u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  566. require.NoError(t, err)
  567. assert.Equal(t, int64(2), u.TokenVersion)
  568. // 第三次重放第一步就签下的 r1 → 401,DB 不得再 +1。
  569. _, err = NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
  570. Authorization: "Bearer " + r1.RefreshToken,
  571. })
  572. require.Error(t, err)
  573. var ce *response.CodeError
  574. require.True(t, errors.As(err, &ce))
  575. assert.Equal(t, 401, ce.Code(),
  576. "重放旧 refreshToken 必须 401;服务端绝不得因签 token 副作用推进 DB")
  577. u2, err := svcCtx.SysUserModel.FindOne(ctx, userId)
  578. require.NoError(t, err)
  579. assert.Equal(t, int64(2), u2.TokenVersion, "重放失败分支不得推进 tokenVersion")
  580. }