diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java new file mode 100644 index 000000000..4b2628439 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthFailedService.java @@ -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); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java index df0b1867b..d4c9a1611 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java @@ -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 LocalAuthService(LocalCredentialRepository credentialRepository, this.passwordPolicyValidator = passwordPolicyValidator; this.passwordEncoder = passwordEncoder; this.clock = clock; + this.localAuthFailedService = localAuthFailedService; } /** @@ -126,7 +130,7 @@ public PlatformPrincipal login(String username, String password) { ensureNotLocked(credential); if (!passwordEncoder.matches(password, credential.getPasswordHash())) { - handleFailedLogin(credential); + localAuthFailedService.handleFailedLogin(credential.getId()); throw invalidCredentials(); } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java index 8346b9c23..19d2ce56d 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java @@ -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 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 + ); + } diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java index 6b9f43446..6ffd33e20 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java @@ -51,6 +51,9 @@ class LocalAuthServiceTest { @Mock private PasswordEncoder passwordEncoder; + @Mock + private LocalAuthFailedService localAuthFailedService; + private LocalAuthService service; @BeforeEach @@ -62,7 +65,8 @@ void setUp() { globalNamespaceMembershipService, new PasswordPolicyValidator(), passwordEncoder, - CLOCK + CLOCK, + localAuthFailedService ); } @@ -122,8 +126,7 @@ void login_withInvalidPassword_incrementsCounter() { .extracting("status") .isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(credential.getFailedAttempts()).isEqualTo(1); - verify(credentialRepository).save(credential); + verify(localAuthFailedService).handleFailedLogin(credential.getId()); } @Test @@ -141,7 +144,7 @@ void login_afterMaxFailures_setsLockUsingInjectedClock() { .extracting("status") .isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(credential.getLockedUntil()).isEqualTo(Instant.now(CLOCK).plusSeconds(15 * 60)); + verify(localAuthFailedService).handleFailedLogin(credential.getId()); } @Test