fix(auth): avoid gap locks when creating accounts

This commit is contained in:
2026-09-30 11:29:02 +08:00
parent c7945ad8fd
commit 6731be1a6b
2 changed files with 54 additions and 4 deletions
@@ -20,6 +20,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.dao.DuplicateKeyException;
import jakarta.annotation.Resource;
@@ -85,7 +86,7 @@ class AccountManagementServiceTest {
}
@Test void createPlatformAccountPersistsPlatformAppCodeAndPasswordHashOnly() {
when(repository.findById(20L)).thenReturn(null);
when(repository.findCurrentById(20L)).thenReturn(null);
when(repository.existsByUsername("platform.user")).thenReturn(false);
var result = service.create(new CreateAccountRequest(20L, "platform.user", "Password1",
@@ -97,6 +98,7 @@ class AccountManagementServiceTest {
assertEquals("PLATFORM", captor.getValue().getDefaultAppCode());
assertTrue(encoder.matches("Password1", captor.getValue().getPasswordHash()));
assertNotEquals("Password1", captor.getValue().getPasswordHash());
verify(repository, never()).findById(20L);
}
@Test void usernameAvailabilityUsesTheAuthAccountRepositoryAsTheFactSource() {
@@ -107,7 +109,7 @@ class AccountManagementServiceTest {
}
@Test void createDisabledAccountAndSummaryExposeLastLoginAndScope() {
when(repository.findById(20L)).thenReturn(null);
when(repository.findCurrentById(20L)).thenReturn(null);
when(repository.existsByUsername("merchant.user")).thenReturn(false);
var created = service.create(new CreateAccountRequest(20L, "merchant.user", "Password1",
@@ -141,6 +143,7 @@ class AccountManagementServiceTest {
account.setUsername("platform.user"); account.setDisplayName("平台用户");
account.setDefaultTenantId(0L); account.setOperatorTenantId(0L);
account.setDefaultMembershipId(30L); account.setDefaultAppCode("PLATFORM"); account.setDeleted(0);
when(repository.findCurrentById(10L)).thenReturn(account);
var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1",
"平台用户", 0L, 30L, "PLATFORM"));
@@ -149,6 +152,44 @@ class AccountManagementServiceTest {
verify(repository, never()).insert(any());
}
@Test void concurrentInsertOfSameIdentityReturnsDurableWinner() {
account.setUsername("platform.user"); account.setDefaultTenantId(0L);
account.setOperatorTenantId(0L); account.setDefaultMembershipId(30L);
account.setDefaultAppCode("PLATFORM"); account.setDeleted(0);
when(repository.findCurrentById(10L)).thenReturn(null);
doThrow(new DuplicateKeyException("concurrent insert")).when(repository).insert(any());
var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1",
"平台用户", 0L, 30L, "PLATFORM"));
assertEquals(10L, result.accountId());
var order = inOrder(repository);
order.verify(repository).findCurrentById(10L);
order.verify(repository).existsByUsername("platform.user");
order.verify(repository).insert(any());
order.verify(repository).findById(10L);
verify(repository, never()).save(any());
}
@Test void concurrentInsertOfDifferentIdentityIsRejected() {
when(repository.findCurrentById(10L)).thenReturn(null);
doThrow(new DuplicateKeyException("concurrent insert")).when(repository).insert(any());
assertThrows(BizException.class, () -> service.create(new CreateAccountRequest(10L,
"platform.user", "Password1", "平台用户", 0L, 30L, "PLATFORM")));
verify(repository, never()).save(any());
}
@Test void concurrentUsernameConflictDoesNotCreateOrReturnAnotherAccount() {
when(repository.findCurrentById(20L)).thenReturn(null);
when(repository.findById(20L)).thenReturn(null);
doThrow(new DuplicateKeyException("username conflict")).when(repository).insert(any());
assertThrows(BizException.class, () -> service.create(new CreateAccountRequest(20L,
"platform.user", "Password1", "平台用户", 0L, 30L, "PLATFORM")));
verify(repository, never()).save(any());
}
@Test void synchronizeDisplayNameRevokesExistingSessions() {
account.setDisplayName("旧名称");