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

