写在前面

本篇涉及两个场景

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

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

一、接口设计

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));
    }