feat: v2 版本 #206
@@ -166,6 +166,9 @@ func (s *authService) Refresh(ctx context.Context, req RefreshRequest) (*AuthRes
|
|||||||
userID, err := s.userRepo.FindRefreshToken(ctx, tokenHash)
|
userID, err := s.userRepo.FindRefreshToken(ctx, tokenHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, store.ErrRefreshTokenNotFound) {
|
if errors.Is(err, store.ErrRefreshTokenNotFound) {
|
||||||
|
// JWT 校验已通过但 DB 中不存在 → token 已被 rotation 删除,属于复用行为
|
||||||
|
// 吊销该用户全部 refresh token,强制所有设备重新登录
|
||||||
|
_ = s.userRepo.DeleteUserRefreshTokens(ctx, claims.UserID)
|
||||||
return nil, ErrRefreshTokenUsed
|
return nil, ErrRefreshTokenUsed
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -188,3 +188,48 @@ func TestLogout_Success(t *testing.T) {
|
|||||||
})
|
})
|
||||||
assert.ErrorIs(t, err, auth.ErrRefreshTokenUsed)
|
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 失败已间接验证
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user