SpringBoot集成SpringSecurity(六)实现登录验证码

登录验证码

有很多种实现形式,如自定义类UsernamePasswordAuthenticationFilter(默认的用户认证过滤器)、在UsernamePasswordAuthenticationFilter前添加验证码拦截器等。

下面通过第二种实现,在UsernamePasswordAuthenticationFilter前添加验证码拦截器,如果验证码不通过,那么不继续后面的认证。

验证码实现接口

通过hutool插件实现图片验证码,并将验证码写入到session中,key为verifycode,后面校验时需要从session的key中获取验证码真实内容。

@RequestMapping("/getver")
@ResponseBody
public void verifycode(HttpServletResponse response, HttpSession session) {
    LineCaptcha captcha = CaptchaUtil.createLineCaptcha(100, 30, 4, 0);
    session.setAttribute("verifycode", captcha.getCode());
    System.out.println(session);
    System.out.println(captcha.getCode());
    SecurityContextHolder.clearContext();
    try {
        ServletOutputStream outputStream = response.getOutputStream();
        captcha.write(outputStream);
        outputStream.flush();
        outputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

自定义验证码过滤器

自定义验证码过滤器VerifyCodeFilter实现GenericFilter,在VerifyCodeFilter中注入一个MyLoginFailureHandler登录失败处理器来处理验证码错误异常。前端发送的验证码输入框的name必须是code。

@Component
public class VerifyCodeFilter extends GenericFilter {
​
    public final static String CODE_FROM_WEB = "code";
    public final static String CODE_FROM_SESSION = "verifycode";
​
    @Autowired
    private MyLoginFailureHandler myLoginFailureHandler;
​
    private String defaultFilterProcessUrl = "/login";
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getRequestURI())) {
            String code = request.getParameter(CODE_FROM_WEB);
            String verifycode = (String) request.getSession().getAttribute(CODE_FROM_SESSION);
            System.out.println(code + " " + verifycode);
            try {
                validate(code, verifycode);
                request.getSession().removeAttribute(CODE_FROM_SESSION);
            } catch (VerifyCodeException e) {
                myLoginFailureHandler.onAuthenticationFailure(request, response, e);
                return;//如果验证码错误那么直接返回,非常重要。
            }
        }
        filterChain.doFilter(request, response);
    }
​
    public void validate(String code, String verifycode) throws InsufficientAuthenticationException {
        if(StrUtil.isEmpty(code) || StrUtil.isEmpty(verifycode) || !verifycode.equalsIgnoreCase(code.toLowerCase())) {
            throw new VerifyCodeException("验证码错误");
        }
    }
}
​//定义验证码错误异常,需要继承AuthenticationException
class VerifyCodeException extends AuthenticationException {
    public VerifyCodeException(String msg) {
        super(msg);
    }
}

丰富MyLoginFailureHandler

MyLoginFailureHandler就是之前定义的用户登陆失败Handler,添加对VerifyCodeException的处理。

@Component
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        HashMap<String, Object> map = new HashMap<>();
        String a = "";
        if(e instanceof BadCredentialsException) {
            a = "密码错误";
        } else if(e instanceof DisabledException) {
            a = "账户被禁用";
        } else if(e instanceof AccountExpiredException) {
            a = "账户已过期";
        } else if(e instanceof LockedException) {
            a = "账户被锁定";
        } else if(e instanceof CredentialsExpiredException) {
            a = "账户凭证过期";
        } else if(e instanceof UsernameNotFoundException) {
            //在springsecurity中UsernameNotFoundException被屏蔽无法使用,用户找不到会抛BadCredentialsException
            a = "账户不存在";
        } else if(e instanceof SessionAuthenticationException && e.getMessage().startsWith("Maximum sessions")){
            a = "您已在其他设备登录,禁止登录";
        } else if(e instanceof VerifyCodeException){
            a = "验证码错误";
        } else {
            a = "未知错误";
        }
        System.out.println(e.getMessage());
        map.put("code", "401");
        map.put("message", "登录失败! " + a);
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(map));
    }
}

配置验证码过滤器生效

配置VerifyCodeFilter在UsernamePasswordAuthenticationFilter认证之前添加自定义过滤器,匹配验证码过滤器。此时在进入UsernamePasswordAuthenticationFilter之前执行verifyCodeFilter进行验证码验证,如果验证码不通过那么不执行后续认证。

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...省略代码
    http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
    ...省略代码
    }

测试分析过程:

  1. 在postman中访问http://localhost:8080/getver获取验证码;
  2. 访问http://localhost:8080/login?username=admin&password=123&code=8d1f,如果不加code或输入的code错误,那么抛出401错误,登录失败验证码错误。
  3. 访问http://localhost:8080/login?username=admin&password=123&code=8d1f,code正确时提示登录成功。

版权声明:本文为xinshengdelei原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。