You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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());
  }

}

问题原因分析

  1. 事务上下文冲突:事件监听方法onFailure默认无事务上下文,调用标记@Transactional的registerLoginFailure时,事务传播行为为REQUIRED,可能复用登录流程的事务(若存在),导致事务提交时机异常或更新被回滚。
  2. 嵌套事务失效:lockClientAccount方法自身标记@Transactional,与外层registerLoginFailure的事务形成嵌套。Spring默认JPA事务管理器不支持真正的嵌套事务,仅会沿用外层事务,若外层事务因其他逻辑未提交,更新操作无法持久化。
  3. 并发场景下的事务隔离:并发登录失败请求中,多个线程同时读取到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) {
    // 执行发送邮件等后续操作
}

验证建议

  1. 模拟并发登录失败场景,观察数据库中accountNoneLocked字段是否正确更新
  2. 查看事务日志,确认事务是否正常提交
  3. 检查Redis中BruteForceEntity的失败次数是否正确累加,确保逻辑走到锁定步骤

内容的提问来源于stack exchange,提问作者iTchTheRightSpot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 19:05:10