SpringSecurity下,使用Redis实现验证码验证,用户错误登陆次数限制,锁定/释放用户
写在前面
本篇涉及两个场景
- 验证码验证逻辑
- 错误登录控制(锁定/释放用户)
本篇只是对这两种场景的一种实现,可供参考,还有别的实现方式,可自行学习探索、使用
一、接口设计
1.1、验证码接口
1.2、登陆接口
二、验证码验证逻辑
2.1、验证码生成,几种生成方式可供参考,参考链接
参考中都是写到文件,实际使用时,是请求验证码生成接口,接口响应写到输出流到页面
/**
* 生成验证码
*/@RestControllerpublicclassCaptchaImageController{@Autowiredprivate StringRedisTemplate stringRedisTemplate;@GetMapping("/code/image")public ResultBeancreateCode(String captchaId, HttpServletRequest request, HttpServletResponse response)throws IOException{if(StringUtil.isNullStr(captchaId)){return ResultBean.error(CodeEnum.CUSTON_ERROR,"缺少参数");}// 设置大小,以及位数
SpecCaptcha specCaptcha=newSpecCaptcha(129,48,4);// 设置字体
specCaptcha.setFont(newFont("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 ResultBeanlogin(
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=newUsernamePasswordAuthenticationToken(username, password);
authentication= authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String token= jwtTokenProvider.generateToken(username);
Cookie cookie=newCookie(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));}