From 898e30b5266e5410c7a0521c3421b7b7402cfe00 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Sat, 20 Jun 2026 14:51:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Refresh=20Token=20=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=20=E2=80=94=20=E6=A3=80=E6=B5=8B=E5=88=B0?= =?UTF-8?q?=E5=B7=B2=20rotation=20=E7=9A=84=20token=20=E8=A2=AB=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E5=90=8A=E9=94=80=E7=94=A8=E6=88=B7=E5=85=A8?= =?UTF-8?q?=E9=83=A8=20refresh=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refresh 方法中 FindRefreshToken 返回 not found 时,检查 JWT 是否有效 - JWT 有效但 DB 不存在 → 判定为复用,调用 DeleteUserRefreshTokens 吊销该用户所有 token - 增加 TestRefresh_ReuseDetectedRevokesAllTokens 测试覆盖复用场景 --- backend/internal/auth/service.go | 3 ++ backend/internal/auth/service_test.go | 45 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 4f18447..0cd3f3f 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -166,6 +166,9 @@ func (s *authService) Refresh(ctx context.Context, req RefreshRequest) (*AuthRes userID, err := s.userRepo.FindRefreshToken(ctx, tokenHash) if err != nil { if errors.Is(err, store.ErrRefreshTokenNotFound) { + // JWT 校验已通过但 DB 中不存在 → token 已被 rotation 删除,属于复用行为 + // 吊销该用户全部 refresh token,强制所有设备重新登录 + _ = s.userRepo.DeleteUserRefreshTokens(ctx, claims.UserID) return nil, ErrRefreshTokenUsed } return nil, err diff --git a/backend/internal/auth/service_test.go b/backend/internal/auth/service_test.go index 9fc91a5..ab14554 100644 --- a/backend/internal/auth/service_test.go +++ b/backend/internal/auth/service_test.go @@ -188,3 +188,48 @@ func TestLogout_Success(t *testing.T) { }) assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed) } + +// --- Refresh Token 复用检测 --- + +func TestRefresh_ReuseDetectedRevokesAllTokens(t *testing.T) { + svc, repo := newTestService(t) + ctx := context.Background() + + // 注册,获得令牌对 A + regResp, err := svc.Register(ctx, auth.RegisterRequest{ + Username: "eve", + Password: "password123", + }) + require.NoError(t, err) + tokenPairA_refresh := regResp.RefreshToken + + // 再次登录,获得令牌对 B + loginResp, err := svc.Login(ctx, auth.LoginRequest{ + Username: "eve", + Password: "password123", + }) + require.NoError(t, err) + tokenPairB_refresh := loginResp.RefreshToken + + // 用令牌对 A 的 refresh token 正常刷新 → 成功 + refreshResp, err := svc.Refresh(ctx, auth.RefreshRequest{ + RefreshToken: tokenPairA_refresh, + }) + require.NoError(t, err) + assert.NotEmpty(t, refreshResp.AccessToken) + + // 用令牌对 A 的旧 refresh token 再次刷新 → 复用检测,应失败 + _, err = svc.Refresh(ctx, auth.RefreshRequest{ + RefreshToken: tokenPairA_refresh, + }) + assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed) + + // 令牌对 B 的 refresh token 也应被吊销(全量吊销) + _, err = svc.Refresh(ctx, auth.RefreshRequest{ + RefreshToken: tokenPairB_refresh, + }) + assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed) + + // 确认 DB 中该用户已无 refresh token + _ = repo // repo 用于确认,但 MemUserRepository 无直接查询方法,通过 Refresh 失败已间接验证 +}