SpringSecurity下,使用Redis实现验证码验证,用户错误登陆次数限制,锁定/释放用户
写在前面
本篇涉及两个场景
- 验证码验证逻辑
- 错误登录控制(锁定/释放用户)
本篇只是对这两种场景的一种实现,可供参考,还有别的实现方式,可自行学习探索、使用
一、接口设计
1.1、验证码接口
1.2、登陆接口
二、验证码验证逻辑
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));
}