Spring Security中@EventListener方法的JPA数据修改未持久化问题
暴力破解防护机制:登录失败事件中无法持久化Clientz锁定状态
问题概述
实现小型项目的暴力破解攻击防护时,通过@EventListener监听登录失败事件触发的registerLoginFailure方法,虽标记了@Transactional且能成功查询到Clientz实体,但更新该实体locked属性的操作无法持久化到数据库。具体表现为:
- 单步调试或通过PUT REST请求调用
lockClientAccount方法时功能正常 - 多次/并发错误凭证登录请求场景下,锁定状态更新失败
相关代码
BruteForceService 核心逻辑
import org.springframework.transaction.annotation.Transactional; /** * Method to register every login failure of type BadCredentials for a Clientz. * this.bruteForceRepo stores in redis for caching and concurrency * @param auth of type Spring Core Authentication */ @Transactional public void registerLoginFailure(Authentication auth) { var clientz = this.clientRepository.findByPrincipal(auth.getName()).orElse(null); if (clientz == null || !clientz.accountNoneLocked()) { return; } // Using redis for caching and concurrency var bruteEntity = this.bruteForceRepo.findByPrincipal(auth.getName()); if (bruteEntity.isEmpty()) { this.bruteForceRepo.save(new BruteForceEntity(0, auth.getName())); return; } if (bruteEntity.get().getFailedAttempt() < this.MAX) { bruteEntity.get().setFailedAttempt(bruteEntity.get().getFailedAttempt() + 1); this.bruteForceRepo.update(bruteEntity.get()); return; } this.clientRepository.lockClientAccount(false, clientz.getClientId()); // TODO send client email to change password }
ClientRepository 更新方法
import org.springframework.data.jpa.repository.Modifying; import org.springframework.transaction.annotation.Transactional; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @Modifying(flushAutomatically = true, clearAutomatically = true) @Transactional @Query(value = "UPDATE Clientz c SET c.accountNoneLocked = ?1 WHERE c.clientId = ?2") void lockClientAccount(@Param(value = "bool") boolean bool, @Param(value = "id") Long id);
事件监听触发逻辑
import org.springframework.context.event.EventListener; import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; import org.springframework.stereotype.Component; @Component public class AuthenticationEvents { @EventListener public void onFailure(AbstractAuthenticationFailureEvent failures) { bruteForceService.registerLoginFailure(failures.getAuthentication()); } }
问题原因分析
- 事务上下文冲突:事件监听方法
onFailure默认无事务上下文,调用标记@Transactional的registerLoginFailure时,事务传播行为为REQUIRED,可能复用登录流程的事务(若存在),导致事务提交时机异常或更新被回滚。 - 嵌套事务失效:
lockClientAccount方法自身标记@Transactional,与外层registerLoginFailure的事务形成嵌套。Spring默认JPA事务管理器不支持真正的嵌套事务,仅会沿用外层事务,若外层事务因其他逻辑未提交,更新操作无法持久化。 - 并发场景下的事务隔离:并发登录失败请求中,多个线程同时读取到
clientz.accountNoneLocked = true并执行锁定操作,可能因默认事务隔离级别(READ_COMMITTED)导致部分更新被覆盖或未生效。
解决方案
方案1:给事件监听方法添加独立事务
修改AuthenticationEvents的监听方法,强制使用独立事务,避免与其他事务上下文冲突:
@Component public class AuthenticationEvents { @EventListener @Transactional(propagation = Propagation.REQUIRES_NEW) public void onFailure(AbstractAuthenticationFailureEvent failures) { bruteForceService.registerLoginFailure(failures.getAuthentication()); } }
方案2:移除Repository方法的事务注解
由于registerLoginFailure已标记@Transactional,移除lockClientAccount的@Transactional注解,避免嵌套事务问题:
@Modifying(flushAutomatically = true, clearAutomatically = true) @Query(value = "UPDATE Clientz c SET c.accountNoneLocked = ?1 WHERE c.clientId = ?2") void lockClientAccount(@Param(value = "bool") boolean bool, @Param(value = "id") Long id);
方案3:优化并发场景下的更新逻辑
为防止并发请求绕过锁定检查,在lockClientAccount的JPQL中添加条件判断,确保仅当账户未被锁定时才执行更新:
@Modifying(flushAutomatically = true, clearAutomatically = true) @Query(value = "UPDATE Clientz c SET c.accountNoneLocked = ?1 WHERE c.clientId = ?2 AND c.accountNoneLocked = true") int lockClientAccount(@Param(value = "bool") boolean bool, @Param(value = "id") Long id);
同时,在Service中通过返回值判断是否成功执行更新,避免无效操作:
int affectedRows = this.clientRepository.lockClientAccount(false, clientz.getClientId()); if (affectedRows > 0) { // 执行发送邮件等后续操作 }
验证建议
- 模拟并发登录失败场景,观察数据库中
accountNoneLocked字段是否正确更新 - 查看事务日志,确认事务是否正常提交
- 检查Redis中
BruteForceEntity的失败次数是否正确累加,确保逻辑走到锁定步骤
内容的提问来源于stack exchange,提问作者iTchTheRightSpot
相关产品推荐
相关产品推荐

