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

Spring环境下登录失败计数的事务处理与事件触发问题

问题解决思路

一、事务不独立的问题解决

你遇到的REQUIRES_NEW不生效,核心原因是Spring事务基于动态代理实现,同一类内部的方法调用不会触发代理逻辑。因为你的UserAuthenticationService类级加了@Transactional,validatePassword调用updatePasswordAttempt时,直接走的是对象内部方法,不会创建新事务,所以外层抛异常时,更新操作也会跟着回滚。

解决方法有两种:

1. 拆分方法到独立服务类

把updatePasswordAttempt方法移到一个新的服务类(比如UserPasswordAttemptManager),确保调用时走代理:

@Service
public class UserPasswordAttemptManager {
    @Autowired
    private UserRepository userRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updatePasswordAttempt(Long userId, Integer attemptCount) {
        userRepository.updatePasswordAttempt(userId, attemptCount);
    }
}

然后在UserAuthenticationService中注入这个类调用:

@Service
@Transactional
public class UserAuthenticationService {
    @Autowired
    private UserPasswordAttemptManager attemptManager;

    public void validatePassword(String password, UserEntity user) {
        if(Boolean.TRUE.equals(matchPassword(password, user.getPassword()))){
            if(user.getPasswordAttempt().equals(0)) return;
            attemptManager.updatePasswordAttempt(user.getId(), 0);
            return;
        }

        int attemptCount = user.getPasswordAttempt() + 1;
        attemptManager.updatePasswordAttempt(user.getId(), attemptCount);
        throw new AuthException(INVALID_PASSWORD.getMessage(), INVALID_PASSWORD.getCode());
    }
}

2. 注入自身代理对象调用

如果不想拆分类,可以在UserAuthenticationService中注入自身的代理实例,通过代理调用updatePasswordAttempt:

@Service
@Transactional
public class UserAuthenticationService {
    @Autowired
    private UserRepository userRepository;
    // 注入自身代理
    @Autowired
    private UserAuthenticationService self;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updatePasswordAttempt(Long userId, Integer attemptCount){
        userRepository.updatePasswordAttempt(userId, attemptCount);
    }

    public void validatePassword(String password, UserEntity user){
        if(Boolean.TRUE.equals(matchPassword(password, user.getPassword()))){
            if(user.getPasswordAttempt().equals(0)) return;
            // 通过代理调用,触发新事务
            self.updatePasswordAttempt(user.getId(), 0);
            return;
        }

        int attemptCount = user.getPasswordAttempt() + 1;
        self.updatePasswordAttempt(user.getId(), attemptCount);
        throw new AuthException(INVALID_PASSWORD.getMessage(), INVALID_PASSWORD.getCode());
    }
}

二、Spring事件相关问题解答

1. 事件设计是否合理?

如果用事件解耦认证逻辑和计数更新逻辑,是可行的,但你担心的循环依赖问题可以避免:不要让事件监听类依赖UserAuthenticationService,而是直接注入UserRepository或者刚才拆分的UserPasswordAttemptManager,直接完成计数更新,这样就不会出现循环依赖了。

2. 自动触发事件的方式

Spring Security自带认证失败事件,你不需要手动发布:

  • 当认证失败(比如密码错误)时,Spring Security会发布AuthenticationFailureBadCredentialsEvent事件
  • 你可以写一个监听器监听这个事件,自动更新密码尝试次数:
@Component
public class AuthenticationFailureListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
    @Autowired
    private UserPasswordAttemptManager attemptManager;
    @Autowired
    private UserRepository userRepository;

    @Override
    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
        String username = event.getAuthentication().getName();
        UserEntity user = userRepository.findByUsername(username);
        if(user != null){
            int attemptCount = user.getPasswordAttempt() + 1;
            attemptManager.updatePasswordAttempt(user.getId(), attemptCount);
        }
    }
}

另外,如果是认证成功(密码正确),可以监听AuthenticationSuccessEvent,重置密码尝试次数:

@Component
public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {
    @Autowired
    private UserPasswordAttemptManager attemptManager;
    @Autowired
    private UserRepository userRepository;

    @Override
    public void onApplicationEvent(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        UserEntity user = userRepository.findByUsername(username);
        if(user != null && user.getPasswordAttempt() > 0){
            attemptManager.updatePasswordAttempt(user.getId(), 0);
        }
    }
}

这种方式完全不需要在你的validatePassword方法里手动处理计数,由Spring Security的事件驱动完成,解耦性更好。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 19:10:48