fix(auth): avoid gap locks when creating accounts
This commit is contained in:
@@ -9,6 +9,7 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -63,7 +64,9 @@ public class AccountManagementService {
|
||||
String appCode = request.appCode() == null ? "MERCHANT"
|
||||
: request.appCode().trim().toUpperCase(Locale.ROOT);
|
||||
validateScope(appCode, request.tenantId());
|
||||
AuthAccount existing = repository.findById(request.accountId());
|
||||
// Locking a missing key takes an InnoDB gap lock and can deadlock
|
||||
// unrelated account inserts. The unique indexes arbitrate creation.
|
||||
AuthAccount existing = repository.findCurrentById(request.accountId());
|
||||
if (existing != null) {
|
||||
if (sameIdentity(existing, request, appCode)) return summary(existing);
|
||||
throw new BizException(ResultCode.CONFLICT, "认证账号标识已被其他成员使用");
|
||||
@@ -88,7 +91,13 @@ public class AccountManagementService {
|
||||
account.setDefaultMembershipId(request.membershipId());
|
||||
account.setDefaultAppCode(appCode);
|
||||
account.setDeleted(0);
|
||||
try {
|
||||
repository.insert(account);
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
AuthAccount winner = repository.findById(request.accountId());
|
||||
if (winner != null && sameIdentity(winner, request, appCode)) return summary(winner);
|
||||
throw new BizException(ResultCode.CONFLICT, "认证账号标识或登录账号已被使用");
|
||||
}
|
||||
return summary(account);
|
||||
}
|
||||
|
||||
|
||||
@@ -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("旧名称");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user