图片验证码登录验证

  • 1.验证码流程详解
  • 2.验证码生成
  • 3.验证码校验


1.验证码流程详解

验证码流程图解析:

  1. 客户端打开登陆页的时候就要发送一个生成图片验证码的请求
  2. 服务端接受请求,就要随机生成验证码图片,将图片验证码响应给前端页面,并且要将生成的验证码保存到session中,以便登录验证校验
  3. 客户端收到验证码图片后,填入表单信息后,发送登录请求
  4. 服务端在接受到前端传来的验证码参数,要先与session中的比对,如果相同,则响应正确,如果不匹配,则返回相应错误信息,如验证码不匹配

本次系统测试效果:
本次的系统是继上一篇springsecurity的案例系统,添加了图片验证码的功能,如果想做参考:上篇博客入口

可以看到测试效果,如果验证码为空,则提示验证码为空,如果验证码错误,则提示验证码不匹配,如果匹配成功,则可以登录,如果已经登录过后,直接返回刚刚的登录页面,再次使用之前的验证码登录,会提示验证码不存在,保证了验证码的一次使用性(一个验证码只能登录一次)!

springsecurity整验证码 spring security验证码登录_spring

2.验证码生成

可以用controller处理生成验证码的请求:

@RestController
public class CheckCodeController {
    @RequestMapping("/getCode")
    public void getCode(HttpServletRequest request, HttpServletResponse response){
        //生成对应宽高的初始图片
        int width=130;
        int height=45;
        BufferedImage img = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);

        //美化图片
        Graphics g = img.getGraphics();
        g.setColor(Color.white);      //设置画笔颜色-验证码背景色
        g.fillRect(0, 0, width, height);//填充背景
        Random ran = new Random();
        //产生4个随机验证码,12Ey
        String checkCode = getCheckCode();
        //将验证码放入HttpSession中
        request.getSession().setAttribute("checkCode_session",checkCode);

        Color color = new Color(ran.nextInt(256),
                ran.nextInt(256), ran.nextInt(256));//随机生成颜色
        g.setColor(color);
        //设置字体的小大
        g.setFont(new Font("微软雅黑", Font.BOLD, 40)   );
        //向图片上写入验证码
        g.drawString(checkCode,15,33);

        //画干扰线
        for (int i = 0; i <6; i++) {
            // 设置随机颜色
            Color color1 = new Color(ran.nextInt(256),
                    ran.nextInt(256), ran.nextInt(256));//随机生成颜色
            g.setColor(color1);
            // 随机画线
            g.drawLine(ran.nextInt(width), ran.nextInt(height),
                    ran.nextInt(width), ran.nextInt(height));
        }
        //添加噪点
        for(int i=0;i<30;i++){

            int x1 = ran.nextInt(width);

            int y1 = ran.nextInt(height);

            Color color2 = new Color(ran.nextInt(256),
                    ran.nextInt(256), ran.nextInt(256));//随机生成颜色
            g.setColor(color2);
            g.fillRect(x1, y1, 2,2);
        }

        //将图片输出页面展示
        try {
            ImageIO.write(img,"png",response.getOutputStream());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //生成随机验证码方法
    private String getCheckCode() {
        String base = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
        int size = base.length();
        Random r = new Random();
        StringBuffer sb = new StringBuffer();
        for(int i=1;i<=4;i++){
            //产生0到size-1的随机值
            int index = r.nextInt(size);
            //在base字符串中获取下标为index的字符
            char c = base.charAt(index);
            //将c放入到StringBuffer中去
            sb.append(c);
        }
        return sb.toString();
    }
}

前端验证码部分的代码:

//表单数据
<span>验证码:<input id="checkCode" type="text" name="checkCode">
<img src="/getCode"  style="width: 130px;height: 40px" onclick="changeCheckCode(this)" ></span>

//图片点击事件
  function changeCheckCode(img) {
      img.src="/getCode?"+new Date().getTime();
      //拼接时间,是为了可以一直刷新验证码,也可以用其他随机数
  }
//Ajax请求,将表单提交,提交按钮点击事件出发login()方法就行
    function login() {
        var username=$("#username").val();
        var password=$("#password").val();
        var checkCode=$("#checkCode").val();
        var rememberMe=$("#remember-me").is(":checked");
        if(username == "" || password == ""){
            alert("用户名或密码不能为空")
        }
        $.post("/login",{"username":username,"password":password,"checkCode":checkCode,"remember":rememberMe},function (data) {
            if (data.isok){
                //成功
                location.href="/index";
            }else {
                //失败
                alert(data.msg);
                location.href="/login.html"
            }

        })
    }

springsecurity还需要配置验证码的请求权限:

.authorizeRequests()
                .antMatchers("/login.html","/login","/getCode").permitAll()

3.验证码校验

首先写一个验证码校验的过滤器:

import com.xuhao.springsecurity.auth.MyFailureHandler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thymeleaf.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;

@Component
public class CheckCodeFilter extends OncePerRequestFilter {

    @Resource
    private MyFailureHandler myFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        if(StringUtils.equals("/login",request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){

            try{
                //验证谜底与用户输入是否匹配
                validate(request);
            }catch(AuthenticationException e){
                myFailureHandler.onAuthenticationFailure(
                        request,response,e //产生异常交给myFailureHandler处理
                );
                return; //产生异常就不执行后面的过滤器链
            }
        }
        filterChain.doFilter(request,response);
    }

    //校验规则
    private void validate(HttpServletRequest request) throws ServletRequestBindingException {

        HttpSession session = request.getSession();

        String checkCode = request.getParameter("checkCode");
        if(StringUtils.isEmpty(checkCode)){
            throw new SessionAuthenticationException("验证码不能为空");
        }

        // 获取session池中的验证码谜底,session中不存在的情况
        String checkCode_session = (String) session.getAttribute("checkCode_session");
        if(Objects.isNull(checkCode_session)) {
            throw new SessionAuthenticationException("验证码不存在");
        }

        // 请求验证码校验
        if(!StringUtils.equalsIgnoreCase(checkCode_session, checkCode)) {
            throw new SessionAuthenticationException("验证码不匹配");
        }
    }
}

在自定义的登录失败类中处理验证码验证异常:

@Component
public class MyFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Value("${spring.security.loginType}")
    private String loginType;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        String errorMsg = "用户名或者密码输入错误!";//返回的错误信息,默认是登录的错误

        if(exception instanceof SessionAuthenticationException){ //如果异常属于验证码session的异常,则获取异常的信息
            errorMsg = exception.getMessage();
        }
        if (loginType.equalsIgnoreCase("json")) {
            //将返回的对象转换成json数据
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(Result.fail(errorMsg));
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(json);
        }else {
            //重新跳转到登录页面
            super.onAuthenticationFailure(request, response, exception);
        }
    }
}

在自定义的登录成功类中移除session中的验证码,做到登录成功后就不能用这个验证码了,保证一个验证码只能用一次:

@Component
public class MySuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Value("${spring.security.loginType}")
    private String loginType;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

        if (loginType.equalsIgnoreCase("json")){
            HttpSession session = request.getSession();
            //登录成功,删除session中验证码,保证验证码的一次性使用
            session.removeAttribute("checkCode_session");
            //将返回的对象转换成json数据
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(Result.success(null));
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(json);
        }else{
            //跳转到登录之前请求的页面
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

最后在securityConfig中配置验证码过滤器:

//先注入
  @Resource
    private CheckCodeFilter checkCodeFilter;
    
//在用户名密码登录验证过滤器前先执行验证码过滤器
    http.addFilterBefore(checkCodeFilter, UsernamePasswordAuthenticationFilter.class);

至此,基于SpringSecurity的图片验证码登录功能已完全实现~~