| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- package pub
- import (
- "context"
- "errors"
- "sync"
- "sync/atomic"
- "testing"
- authHelper "perms-system-server/internal/logic/auth"
- "perms-system-server/internal/response"
- "perms-system-server/internal/testutil"
- "perms-system-server/internal/types"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // ---------------------------------------------------------------------------
- // 覆盖目标:审计 H-1 修复的 logic 层回归 —— 在 logic 里用 CAS 递增 tokenVersion。
- // 该文件聚焦"并发 refresh 同一旧令牌时的行为":
- // 1) N 个并发 RefreshToken 共用同一把 claims.TokenVersion=0 的 refreshToken,
- // 必须恰好 1 个返回成功;其余 N-1 个被 401 拒绝(字样必为"登录状态已失效")。
- // 2) DB 的 tokenVersion 最终只能递增 1;
- // 3) 明确 CAS 失败时返回的 401 错误是通过 ErrTokenVersionMismatch 路径产出,
- // 与"账号冻结"等 403 分支互不混用。
- // ---------------------------------------------------------------------------
- // TC-0812: H-1 logic 并发回归 —— 并发重放同一个旧 refreshToken,只允许一位胜出。
- func TestRefreshToken_ConcurrentSameToken_SingleWinner(t *testing.T) {
- ctx := context.Background()
- svcCtx := newTestSvcCtx()
- username := "rt_cas_" + testutil.UniqueId()
- userId, cleanUser := insertRefreshTestUser(t, ctx, username, "TestPass123", 1, 2)
- t.Cleanup(cleanUser)
- // 禁用 TokenOpLimiter,以让本测试的变量只剩"并发 CAS 胜负"。
- svcCtx.TokenOpLimiter = nil
- rt, err := authHelper.GenerateRefreshToken(
- svcCtx.Config.Auth.RefreshSecret, svcCtx.Config.Auth.RefreshExpire,
- userId, "", 0,
- )
- require.NoError(t, err)
- // 限制在 6 并发以避免触发 go-zero sqlx breaker(单机 MySQL + breaker 对同批次突发
- // 的并发 UPDATE 容易误伤,生产里 refreshToken 也是 per-user 限频 + CAS 双层保护,
- // 没机会打成这么高的并发)。CAS "唯一胜出" 的契约在 N=6 时已足以钉死。
- const N = 6
- var (
- wg sync.WaitGroup
- okCount int32
- authFailCnt int32
- otherErr atomic.Value
- )
- start := make(chan struct{})
- for i := 0; i < N; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- <-start
- resp, e := NewRefreshTokenLogic(ctx, svcCtx).RefreshToken(&types.RefreshTokenReq{
- Authorization: "Bearer " + rt,
- })
- switch {
- case e == nil && resp != nil:
- atomic.AddInt32(&okCount, 1)
- case e != nil:
- var ce *response.CodeError
- if errors.As(e, &ce) && ce.Code() == 401 &&
- ce.Error() == "登录状态已失效,请重新登录" {
- atomic.AddInt32(&authFailCnt, 1)
- } else {
- otherErr.Store(e)
- }
- }
- }()
- }
- close(start)
- wg.Wait()
- if v := otherErr.Load(); v != nil {
- t.Fatalf("并发 RefreshToken 出现非预期错误:%v", v)
- }
- assert.Equal(t, int32(1), atomic.LoadInt32(&okCount),
- "H-1 会话劫持防线:重放同一旧 refreshToken 的 N 个并发请求必须只有 1 个成功")
- assert.Equal(t, int32(N-1), atomic.LoadInt32(&authFailCnt),
- "其他并发者必须返回 401 '登录状态已失效'")
- // DB 必然只递增 1。
- u, err := svcCtx.SysUserModel.FindOne(ctx, userId)
- require.NoError(t, err)
- assert.Equal(t, int64(1), u.TokenVersion,
- "DB tokenVersion 递增幅度就是 CAS 成功次数 → 只能是 1")
- }
|