SpringSecurity下,使用Redis实现验证码验证,用户错误登陆次数限制,锁定/释放用户

  • ​​写在前面​​
  • ​​一、接口设计​​
  • ​​1.1、验证码接口​​
  • ​​1.2、登陆接口​​
  • ​​二、验证码验证逻辑​​
  • ​​2.1、验证码生成,几种生成方式可供参考,[参考链接](javascript:void(0))​​
  • ​​2.2、验证码文本,临时存储,基于Redis,有效期 1 分钟​​
  • ​​2.3、初次登陆不需要验证码​​
  • ​​2.4、验证码错误不计入错误登录次数​​
  • ​​三、错误登录控制(5次)(锁定/释放用户)​​
  • ​​3.1、使用 Redis 临时存储错误次数(10分钟内,记录连续错误次数,最多五次)​​
  • ​​3.2、10分钟内,连续登陆错误(用户名/密码错误)5次后,锁定用户 4 小时​​
  • ​​3.3、锁定用户,Redis 临时存储 锁定用户记录 4 小时,4h 后自动释放,可重新登陆​​
  • ​​四、详细代码如下​​

写在前面

本篇涉及两个场景

  • 验证码验证逻辑
  • 错误登录控制(锁定/释放用户)

本篇只是对这两种场景的一种实现,可供参考,还有别的实现方式,可自行学习探索、使用

一、接口设计

1.1、验证码接口

SpringSecurity下,使用Redis实现验证码验证,用户错误登陆次数限制,锁定/释放用户_验证码

1.2、登陆接口

SpringSecurity下,使用Redis实现验证码验证,用户错误登陆次数限制,锁定/释放用户_redis_02

二、验证码验证逻辑

2.1、验证码生成,几种生成方式可供参考,​​参考链接​​

参考中都是写到文件,实际使用时,是请求验证码生成接口,接口响应写到输出流到页面

/**
* 生成验证码
*/
@RestController
public class CaptchaImageController {

@Autowired
private StringRedisTemplate stringRedisTemplate;

@GetMapping("/code/image")
public ResultBean createCode(String captchaId, HttpServletRequest request, HttpServletResponse response) throws IOException {

if (StringUtil.isNullStr(captchaId)) {
return ResultBean.error(CodeEnum.CUSTON_ERROR, "缺少参数");
}

// 设置大小,以及位数
SpecCaptcha specCaptcha = new SpecCaptcha(129, 48, 4);
// 设置字体
specCaptcha.setFont(new Font("Times New Roman", Font.ITALIC, 34));
// 设置类型
specCaptcha.setCharType(Captcha.TYPE_NUM_AND_UPPER);

stringRedisTemplate.opsForValue().set(
RedisKeyGen.getCaptcha(captchaId),
specCaptcha.text(),
60,
TimeUnit.SECONDS);
specCaptcha.out(response.getOutputStream());
return null;
}

}

2.2、验证码文本,临时存储,基于Redis,有效期 1 分钟

参考生成代码中,存储

stringRedisTemplate.opsForValue().set(
RedisKeyGen.getCaptcha(captchaId),
specCaptcha.text(),
60,
TimeUnit.SECONDS);

除了Redis临时存储之外,还有以下方式作为存储

  • 使用关系型数据库表作为临时存储,校验功能
  • 使用 Session 临时存储,校验时从 Request 中获取已生成的验证码文本与登录接口传参验证码文本比较

2.3、初次登陆不需要验证码

只是当前业务场景

2.4、验证码错误不计入错误登录次数

验证码可无限刷新登录,验证码错误不计入错误登录控制 / 锁定

三、错误登录控制(5次)(锁定/释放用户)

3.1、使用 Redis 临时存储错误次数(10分钟内,记录连续错误次数,最多五次)

3.2、10分钟内,连续登陆错误(用户名/密码错误)5次后,锁定用户 4 小时

3.3、锁定用户,Redis 临时存储 锁定用户记录 4 小时,4h 后自动释放,可重新登陆

四、详细代码如下

@PostMapping("/login")
public ResultBean login(
HttpServletRequest request,
HttpServletResponse response,
String username,
String password,
String captchaId,
String captchaCode) {
// 验证用户是否被锁定
String lockUser = stringRedisTemplate.opsForValue().get(RedisKeyGen.getLockUser(username));
if (!StringUtil.isNullStr(lockUser)) {
return ResultBean.error(CodeEnum.USER_LOCKED);
}
//在去redis获取登录次数的一个key,有效期10分钟,如果没获取这个key,但是验证码不为空的时候,
// 直接返回提示,验证码已过期,请刷新浏览器,
String loginErrTimes = stringRedisTemplate.opsForValue().get(RedisKeyGen.getLoginErr(username));
if (StringUtil.isNullStr(loginErrTimes) && !StringUtil.isNullStr(captchaCode)) {
return ResultBean.error(CodeEnum.CAPTCHA_EXPIRED_ERROR);
}

Integer loginErrorTime = 0;
if (!StringUtil.isNullStr(loginErrTimes)) {
loginErrorTime = Integer.valueOf(loginErrTimes);
}

// 如果验证吗为空(缓存刷新,首次登陆),那不需判断验证吗,否则如果有,必须判断验证吗是否正确
if (!StringUtil.isNullStr(captchaCode)) {
String code = stringRedisTemplate.opsForValue().get(RedisKeyGen.getCaptcha(captchaId));
if (StringUtil.isNullStr(code)) {
return ResultBean.error(CodeEnum.CAPTCHA_EXPIRED_ERROR);
}
if (!captchaCode.equalsIgnoreCase(code)) { // 忽略大小写
return ResultBean.error(CodeEnum.CAPTCHA_ERROR);
}
}

// 10分钟内,不可连续用户/密码错误 5 次
Authentication authentication = null;
try {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtTokenProvider.generateToken(username);
Cookie cookie = new Cookie(HEADER, token);
cookie.setHttpOnly(true);
cookie.setPath("/");
//设置过期时间4小时
cookie.setMaxAge(4 * 60 * 1000);
cookie.setSecure(request.isSecure());
cookie.setDomain(request.getServerName().toLowerCase());
response.addCookie(cookie);
} catch (AuthenticationException e) {
int i = loginErrorTime + 1;
stringRedisTemplate.opsForValue().set(RedisKeyGen.getLoginErr(username), String.valueOf(i), 10, TimeUnit.MINUTES);
if (i == 4) {
return ResultBean.error(CodeEnum.ERROR_FOUR);
}
if (i >= 5) {
stringRedisTemplate.opsForValue().set(RedisKeyGen.getLockUser(username), "1", 4, TimeUnit.HOURS);
return ResultBean.error(CodeEnum.USER_LOCKED);
}
// stringRedisTemplate.delete(Lists.newArrayList(RedisKeyGen.getUserInfo(username), RedisKeyGen.getUserResource(username)));
return ResultBean.error(CodeEnum.PSAA_ERROR);
}
// 登陆成功,删除缓存的锁定用户和错误登陆次数
stringRedisTemplate.delete(Lists.newArrayList(RedisKeyGen.getLockUser(username), RedisKeyGen.getLoginErr(username)));
return ResultBean.ok(getLoginVO(username));
}