前后端分离开发中的验证码问题

1.环境和工具

  • 后端:JDK 11、Tomcat 9、 maven 3.6.2、MySQL 5.7
  • 前端:node.js 12.13.0、npm 6.12.0、VueCLI构建的SPA、axios网络请求工具

2.方案描述

  • 后端通过工具方法生成随机字符串
  • 封装使用该字符串在缓冲区生成指定大小图片的方法(可以加干扰线、噪点等),本文主要说明问题的解决流程,所以不对此展开过多探讨
  • 同时需要将该字符串存入HttpSession对象中,其SessionId通过response的响应头Access-Token存入,和验证码一起返回给客户端
  • Servlet处理请求,将该图片通过response对象的输出流返回给客户端
  • 客户端在页面加载完毕,通过请求可以拿到该验证码图片的字节流,通过blob的处理,生成一个URL,作为图片的src属性的值,显示验证码图片,同时可以在请求的回调函数中,通过response的headers取到access-token的值,就是和服务器端对应的那个JSESSIONID的值(传统开发中,一般记录在cookie中),但是通过chrome工具可以看到,是个HTTPOnly,所以拿不到。
  • 加上时间戳,可以实现点击图片换一张的效果
  • 点击“登录”按钮,将用户输入的账号、密码、验证码封装成传输对象,一起传到后台,同时在请求头里面加入之前取到的那个JSESSIONID的值。

3.注意点

  • 过滤器需要添加ACCESS-TOKEN的支持
  • 后端的response需要通过响应头将这个session的id返回给客户端,客户端在回调函数可以拿到响应头,取出这个值绑定给一个变量
  • 客户端在第二次发起登录请求的时候,记得在请求头里面加入这个值,到了登录的请求处理代码里面,通过自定义的Session监听方法,可以通过session的id拿到前一次的那个session对象,从而得到之前生成的正确的验证码的值,把它和刚登录的时候传到后端的登录用户的DTO对象里的验证码比对,判断要不要继续下一步登陆验证。

4.关键代码

  • 生成随机字符串
import java.util.Random;

/**
 * @author mq_xu
 * @ClassName StringUtil
 * @Description 字符串工具类
 * @Date 2019/11/14
 * @Version 1.0
 **/
public class StringUtil {

   private final static int MAX = 4;

    public static String getRandomString() {
        StringBuilder stringBuilder = new StringBuilder();
        Random random = new Random();
        int index;
        //生成四位随机字符
        for (int i = 0; i < MAX; i++) {
            //随机生成0、1、2三个整数,代表数字字符、大写字母、小写字母,保证验证码的组成比较正态随机
            index = random.nextInt(3);
            //调用本类封装的私有方法,根据编号获得对应的字符
            char result = getChar(index);
            //追加到可变长字符串
            stringBuilder.append(result);
        }
        return stringBuilder.toString();
    }

    private static char getChar(int item) {
        //数字字符范围
        int digitalBound = 10;
        //字符范围
        int charBound = 26;
        Random random = new Random();
        int index;
        char c;
        //根据调用时候的三个选项,生成数字、大写字母、小写字母三种不同的字符
        if (item == 0) {
            index = random.nextInt(digitalBound);
            c = (char) ('0' + index);
        } else if (item == 1) {
            index = random.nextInt(charBound);
            c = (char) ('A' + index);
        } else {
            index = random.nextInt(charBound);
            c = (char) ('a' + index);
        }
        return c;
    }
}
  • 生成验证码图片
import java.awt.*;
import java.awt.image.BufferedImage;

/**
 * @author mq_xu
 * @ClassName ImageUtil
 * @Description 验证码生成
 * @Date 2019/11/18
 * @Version 1.0
 **/
public class ImageUtil {
    public static BufferedImage getImage(int width, int height, String content) {
        //创建指定大小和图片模式的缓冲图片对象
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //绘图对象
        Graphics2D graphics = (Graphics2D) img.getGraphics();
        //设置颜色
        graphics.setColor(new Color(68, 134, 49));
        //绘制填充矩形
        graphics.fillRect(0, 0, width, height);
        //设置画笔颜色
        graphics.setPaint(new Color(60, 63, 65));
        //设置字体
        Font font = new Font("微软雅黑", Font.BOLD, 40);
        graphics.setFont(font);
        //在指定位置绘制字符串
        graphics.drawString(content, width / 3, height / 2);
        return img;
    }
}
  • 处理验证码请求
import com.scs.web.blog.util.ImageUtil;
import com.scs.web.blog.util.StringUtil;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;

/**
 * @author mq_xu
 * @ClassName CodeController
 * @Description 验证码请求接口
 * @Date 2019/11/14
 * @Version 1.0
 **/
@WebServlet(urlPatterns = {"/api/code"})
public class CodeController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取随机验证码
        String code = StringUtil.getRandomString();
        //存入session
        HttpSession session = req.getSession();
        session.setAttribute("code", code);
        //将sessionId通过响应头传回客户端
        resp.setHeader("Access-Token",session.getId());
         //调过生成验证码图片的方法
        BufferedImage img = ImageUtil.getImage(200, 100, code);
        //设置resp的响应内容类型,前端将是blob
        resp.setContentType("image/jpg");
        //将图片通过输出流返回给客户端
        OutputStream out = resp.getOutputStream();
        ImageIO.write(img, "jpg", out);
        out.close();
    }
}

  • 处理登录请求的核心代码(因为请求地址的不同,此处做了一些封装,没有直接写在doPost()方法里)
        BufferedReader reader = req.getReader();
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
        logger.info("登录用户信息:" + stringBuilder.toString());
        Gson gson = new GsonBuilder().create();
        UserDto userDto = gson.fromJson(stringBuilder.toString(), UserDto.class);
        //客户端输入的验证码
        String inputCode = userDto.getCode().trim();
        //取得客户端请求头里带来的token
        String sessionId = req.getHeader("Access-Token");
        //从自定义的监听代码中取得之前的session对象
        MySessionContext myc = MySessionContext.getInstance();
        HttpSession session = myc.getSession(sessionId);
        //取得当时存入的验证码
        String correctCode = session.getAttribute("code").toString();
        PrintWriter out = resp.getWriter();
        //忽略大小写比对
        if (inputCode.equalsIgnoreCase(correctCode)) {
            //验证码正确,进入登录业务逻辑调用
            Result result = userService.signIn(userDto);
        } else {
            //验证码错误,直接将错误信息返回给客户端,不要继续登录流程了
            Result result = Result.failure(ResultCode.USER_VERIFY_CODE_ERROR);
        }
        out.print(gson.toJson(result));
        out.close();
  • 后端跨域处理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author mq_xu
 * @ClassName CORSFilter
 * @Description 跨域过滤器类
 * @Date 2019/10/3
 * @Version 1.0
 **/
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
    private static Logger logger = LoggerFactory.getLogger(CorsFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        //允许客户端请求头携带
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, Content-Type,Access-Token");
        //允许给客户端响应头携带
        response.setHeader("Access-Control-Expose-Headers", "Access-Token");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {
        logger.info("跨域过滤器初始化");
    }

    @Override
    public void destroy() {
        logger.info("跨域过滤器销毁");
    }

}
  • 前端登录页面(省略CSS样式)



  • 验证结果
    登陆页面启动,会先从后端请求一个验证码,并且拿到响应头里面的sessionid的值


    前端

    查看请求验证码的网络请求,发现响应头里面加入了Access-Token的值


    验证码请求的响应头

可以看下后台的信息,两个id的值一致


后端

登录请求的请求头,可以看到带着的Access-Token


登录请求头

你可能感兴趣的:(前后端分离开发中的验证码问题)