登录失败次数限制
1. 概述
原始的UserService实现类只提供了基本的用户登录功能,没有对登录失败次数进行限制。攻击者可以通过暴力破解的方式不断尝试不同的密码组合来猜测用户密码,存在严重的安全风险。
通过增加用户登录失败次数限制功能,可以防止暴力破解攻击。通过在Redis中记录用户登录失败次数,当失败次数超过设定阈值时锁定账户一段时间,有效提高系统的安全性。
2. 实施方案
2.1 设计思路
通过在Redis中维护用户登录失败次数来实现登录失败次数限制功能:
- 用户每次登录失败时,增加该账户的失败计数
- 当失败次数超过设定阈值时,锁定账户一段时间
- 登录成功后清除失败计数
2.2 Redis存储设计
Key: login:failed:{account}
Value: 失败次数(整数)
Expire: - 未达到最大失败次数:1小时- 达到最大失败次数:锁定时间(默认30分钟)
2.3 配置项
在application-dev.yml中添加了新的配置项:
# 登录失败次数限制配置
login:max-failed-attempts: 5 # 最大失败次数lockout-duration: 1800 # 锁定时间(秒)
2.4 核心方法实现
2.4.1 isAccountLocked方法
/*** 检查账户是否被锁定* @param account 账户名,默认系统中的账户唯一* @return 是否被锁定*/
private boolean isAccountLocked(String account) {String key = LOGIN_FAILED_COUNT_PREFIX + account;Integer failedCount = loginFailedCountRedisUtil.get(key);return failedCount != null && failedCount >= maxFailedAttempts;
}
3.4.2 incrementFailedAttempts方法
/*** 增加登录失败计数* @param account 账户名*/
private void incrementFailedAttempts(String account) {String key = LOGIN_FAILED_COUNT_PREFIX + account;Integer currentCount = loginFailedCountRedisUtil.get(key);int newCount = (currentCount == null) ? 1 : currentCount + 1;// 如果达到最大失败次数,设置较长的过期时间if (newCount >= maxFailedAttempts) {loginFailedCountRedisUtil.set(key, newCount, (long) lockoutDuration);} else {// 否则设置较短的过期时间(1小时)loginFailedCountRedisUtil.set(key, newCount, 3600L);}
}
3.4.3 clearFailedAttempts方法
/*** 清除登录失败计数* @param account 账户名*/
private void clearFailedAttempts(String account) {String key = LOGIN_FAILED_COUNT_PREFIX + account;loginFailedCountRedisUtil.del(key);
}
3.4.4 修改login方法
@Override
public UserInfo login(LoginDTO loginDTO) {final String account = loginDTO.getAccount();final String password = loginDTO.getPassword();// 检查账户是否被锁定if (isAccountLocked(account)) {log.warn("账户已被锁定: {}", account);throw new GlobalException("账户已被锁定,请稍后再试");}// 根据account查询用户final List<User> list = this.list(Wrappers.<User>lambdaQuery().select(User::getId, User::getAvatar, User::getNickname, User::getPassword).eq(User::getAccount, account).ge(User::getStatus, UserStatus.ACTIVATED.getCode()));// 如果用户不存在或同账户的用户多于一条if (list == null || list.size() != 1) {log.error("账户有问题");// 增加登录失败计数incrementFailedAttempts(account);throw new GlobalException("账户有问题");}// 获取到查询到的用户final User user = list.get(0);// 密码不正确if (!BCrypt.checkpw(password, user.getPassword())) {log.error("密码错误");// 增加登录失败计数incrementFailedAttempts(account);throw new GlobalException("登录失败");}// 登录成功,清除失败计数clearFailedAttempts(account);// ... 其他登录逻辑
}
4. 使用示例
4.1 登录失败处理流程
- 用户输入错误的账户或密码
- 系统记录失败次数并存储到Redis
- 当失败次数达到阈值时,账户被锁定
- 锁定期间无法登录,返回错误信息
- 锁定时间过后自动解锁
4.2 登录成功处理流程
- 用户输入正确的账户和密码
- 系统验证通过
- 清除该账户的失败计数
- 正常登录流程
5. 优化效果
5.1 安全性提升
- 有效防止暴力破解攻击
- 限制恶意登录尝试次数
- 提高系统整体安全性
5.2 资源保护
- 减少无效登录请求对系统资源的消耗
- 降低数据库查询压力
- 提高系统稳定性
5.3 用户体验
- 合理的失败次数限制不会影响正常用户
- 明确的错误提示帮助用户理解锁定原因
- 自动解锁机制避免长期锁定
6. 注意事项
- Redis性能: 失败次数存储在Redis中,需要注意Redis的性能和容量
- 配置调整: 根据实际业务需求调整最大失败次数和锁定时间
- 异常处理: 需要妥善处理Redis操作可能出现的异常
- 并发控制: 在高并发场景下需要注意Redis操作的并发控制
