美文网首页前后分离springboot
前后端分离开发中的验证码问题

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

作者: 陶然然_niit | 来源:发表于2019-11-24 19:19 被阅读0次

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样式)
<template>
    <div id="bg">
        <router-link to="/">首页</router-link>
        <div class="login-box">
            <form class="login-form">
                <input type="text" v-model="userDto.mobile" id="mobile" />
                <input type="password" v-model="userDto.password" />
                <div class="code-box">
                    <input type="text" v-model="userDto.code" class="code" />
                    <img class="verify" @click.prevent="refresh" ref="codeImg" />
                </div>
                <input type="button" class="btn btn-lg dark-fill" value="登录" @click="signIn(userDto)" />
                <router-link to="/sign-up">没有账号?去注册</router-link>
            </form>
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            userDto: {
                mobile: '',
                password: '',
                code: ''
            },
            token: ''
        };
    },
    created() {
        this.axios.get(this.GLOBAL.baseUrl + '/code', { responseType: 'blob' }).then(res => {
            // console.log(res);
            var img = this.$refs.codeImg;
            let url = window.URL.createObjectURL(res.data);
            img.src = url;
            console.log(res.headers);
            //取得后台通过响应头返回的sessionId的值
            this.token = res.headers['access-token'];
            console.log(this.token);
        });
    },
    methods: {
        signIn(userDto) {
            this.axios({
                method: 'post',
                url: this.GLOBAL.baseUrl + '/user/sign-in',
                data: JSON.stringify(this.userDto),
                headers: {
                    'Access-Token': this.token  //将token放在请求头带到后端
                }
            }).then(res => {
                if (res.data.msg === '成功') {
                    alert('登录成功');
                    localStorage.setItem('user', JSON.stringify(res.data.data));
                    this.$router.push('/');
                } else {
                    alert(res.data.msg);
                    this.userDto.code = '';
                }
            });
        },
        refresh() {
            this.axios.get(this.GLOBAL.baseUrl + '/code', { responseType: 'blob' }).then(res => {
                console.log(res);
                var img = this.$refs.codeImg;
                let url = window.URL.createObjectURL(res.data);
                img.src = url;
            });
        }
    }
};
</script>
<style scoped>
</style>
  • 验证结果
    登陆页面启动,会先从后端请求一个验证码,并且拿到响应头里面的sessionid的值


    前端

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


    验证码请求的响应头

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


后端

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


登录请求头

相关文章

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

    1.环境和工具 后端:JDK 11、Tomcat 9、 maven 3.6.2、MySQL 5.7 前端:node...

  • 前后端分离开发模式下的接口规范

    1 背景 此处我不解释为什么要前后端分离、前后端分离的优缺点等问题,采用前后端分离开发模式就变成了这样, 前后端分...

  • springboot系列——Swagger整合

    做过前后端分离的童鞋,对于这个插件应该不陌生。在最初的前后端开发模式中,系统开发前前后端的童鞋大家坐下来先定义一下...

  • eladmin前端项目入手2:解决跨域访问

    前后端分离的开发模式下,通常在开发过程中,前后端项目占用不同的端口,这样就会出现前端访问后端数据过程跨域的问题 可...

  • Django rest framework 框架

    开发模式普通开发模式(前后端放一起)前后端分离后端开发为前端提供URL(API/接口的开发)Django中的 FB...

  • ASP.NET Core API CORS

    最近参与一个前后端分离的项目,后端基于 .NET Core 2.1 开发,在进行前后端对接的过程中,被跨域问题折腾...

  • FEZ环境搭建

    摘要: 工作三个月,有幸在不盛行前后端完全分离的狗厂中参与了部门中唯二的两个前后端完全分离项目。开发过程中,发现前...

  • vivo 商城前端架构升级—前后端分离篇

    本文主要以 vivo 商城项目的前后端分离经验,总结前后端分离思路,整理前后端分离方案,以及分离过程中遇到的问题及...

  • Mock.js——强大的后端数据模拟工具

    在我们日常开发中,虽然大多都是前后端分离的开发模式,但是还是会遇到一些前后端协调的问题。比如有时候前端页面已经开发...

  • 前端如何高效的与后端协作开发

    前端如何高效的与后端协作开发 1. 前后端分离 前端与后端的分离,能使前端的开发脱离后端的开发模式,拥有更大的自由...

网友评论

    本文标题:前后端分离开发中的验证码问题

    本文链接:https://www.haomeiwen.com/subject/lrzlwctx.html