fix : 修复登陆失败,事务失效问题

This commit is contained in:
翟二远 2026-04-16 10:30:26 +08:00
parent 89bc58d29e
commit 54f635031f
4 changed files with 88 additions and 6 deletions

View file

@ -0,0 +1,56 @@
package com.iflytek.skillhub.auth.local;
import jakarta.annotation.Nonnull;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
/**
* Handles failed login attempts for local credentials.
* @author zhaieryuan
*/
@Service
public class LocalAuthFailedService {
private static final int MAX_FAILED_ATTEMPTS = 5;
private static final Duration LOCK_DURATION = Duration.ofMinutes(15);
private final Clock clock;
private final LocalCredentialRepository credentialRepository;
public LocalAuthFailedService(Clock clock,
LocalCredentialRepository credentialRepository
){
this.clock = clock;
this.credentialRepository = credentialRepository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void handleFailedLogin(@Nonnull Long credentialId) {
LocalCredential credential = credentialRepository.findById(credentialId)
.orElseThrow(() -> new EntityNotFoundException("Invalid credential id"));
int failedAttempts = credential.getFailedAttempts() + 1;
Instant lockedUntil = credential.getLockedUntil();
if (failedAttempts >= MAX_FAILED_ATTEMPTS && lockedUntil == null) {
lockedUntil = currentTime().plus(LOCK_DURATION);
}
credentialRepository.updateFailedAttemptsAndLockedUntil(credentialId, failedAttempts, lockedUntil);
}
private Instant currentTime() {
return Instant.now(clock);
}
}

View file

@ -45,13 +45,16 @@ public class LocalAuthService {
private final PasswordEncoder passwordEncoder;
private final Clock clock;
private final LocalAuthFailedService localAuthFailedService;
public LocalAuthService(LocalCredentialRepository credentialRepository,
UserAccountRepository userAccountRepository,
UserRoleBindingRepository userRoleBindingRepository,
GlobalNamespaceMembershipService globalNamespaceMembershipService,
PasswordPolicyValidator passwordPolicyValidator,
PasswordEncoder passwordEncoder,
Clock clock) {
Clock clock,
LocalAuthFailedService localAuthFailedService) {
this.credentialRepository = credentialRepository;
this.userAccountRepository = userAccountRepository;
this.userRoleBindingRepository = userRoleBindingRepository;
@ -59,6 +62,7 @@ public class LocalAuthService {
this.passwordPolicyValidator = passwordPolicyValidator;
this.passwordEncoder = passwordEncoder;
this.clock = clock;
this.localAuthFailedService = localAuthFailedService;
}
/**
@ -126,7 +130,7 @@ public class LocalAuthService {
ensureNotLocked(credential);
if (!passwordEncoder.matches(password, credential.getPasswordHash())) {
handleFailedLogin(credential);
localAuthFailedService.handleFailedLogin(credential.getId());
throw invalidCredentials();
}

View file

@ -1,7 +1,10 @@
package com.iflytek.skillhub.auth.local;
import java.time.Instant;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
/**
@ -15,4 +18,20 @@ public interface LocalCredentialRepository extends JpaRepository<LocalCredential
Optional<LocalCredential> findByUserId(String userId);
boolean existsByUsernameIgnoreCase(String username);
/**
* 更新本地凭证的失败尝试次数和锁定截止时间
*
* @param id 凭证ID
* @param failedAttempts 失败尝试次数
* @param lockedUntil 锁定截止时间如果为null表示不锁定
*/
@Modifying
@Query("UPDATE LocalCredential c SET c.failedAttempts = :failedAttempts, c.lockedUntil = :lockedUntil WHERE c.id = :id")
void updateFailedAttemptsAndLockedUntil(
Long id,
int failedAttempts,
Instant lockedUntil
);
}

View file

@ -51,6 +51,9 @@ class LocalAuthServiceTest {
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private LocalAuthFailedService localAuthFailedService;
private LocalAuthService service;
@BeforeEach
@ -62,7 +65,8 @@ class LocalAuthServiceTest {
globalNamespaceMembershipService,
new PasswordPolicyValidator(),
passwordEncoder,
CLOCK
CLOCK,
localAuthFailedService
);
}
@ -122,8 +126,7 @@ class LocalAuthServiceTest {
.extracting("status")
.isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(credential.getFailedAttempts()).isEqualTo(1);
verify(credentialRepository).save(credential);
verify(localAuthFailedService).handleFailedLogin(credential.getId());
}
@Test
@ -141,7 +144,7 @@ class LocalAuthServiceTest {
.extracting("status")
.isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(credential.getLockedUntil()).isEqualTo(Instant.now(CLOCK).plusSeconds(15 * 60));
verify(localAuthFailedService).handleFailedLogin(credential.getId());
}
@Test