大聪明教你学Java | 一文解决因前后端分离导致无法从 Session 中拿到所需数据的问题

491 阅读6分钟

前言

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第26天,点击查看活动详情

🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。

🍊支持作者: 点赞👍、关注💖、留言💌~

这两天大聪明在开发一个小的应用软件,业务逻辑也很简单,其中涉及到一个需求:为了防止恶意提交表单数据,需要在表单中增加一个验证码。本来以为是一个挺简单的需求,但是今天却发现了不对劲的地方...

解决之旅正式开始

问题描述

在提交表单的功能中,验证码从后台生成后返回到前端页面展示,同时为了实现验证码的校验,还需要在生成验证码时,将其存到 session 中,但是在测试提交表单功能的时候却发现了一个神奇的问题:在表单内容校验的 Controller 中获取 session 中存储的验证码值居然得到的是 null 😥... 这一下我就有点麻爪了,不过谁让我是那种迎难而上的人呢(王婆卖瓜自卖自夸~🤭),于是我就开始排查问题原因,折腾了好一阵子,终于发现了问题的根本原因👇

🍋图一🍋

在这里插入图片描述

🍋图二🍋

在这里插入图片描述 通过上面两张截图,我们可以清晰的看到获取验证码的请求和提交表单的请求所对应的 JSESSIONID (JSESSIONID 其实就是 sessionId,直是名字不同)是不一样的,这也就代表着两次请求是毫无关联的,这也就是为什么在表单内容校验的 Controller 中获取 session 中存储的验证码值得到的是 null。说实话这个问题困扰了我好久,期间也是尝试过很多百度上的解决方案,无论是前端的跨域配置,还是后端的跨域配置都试过了,但是问题还是没有解决😔。

功夫不负有心人,柳暗花明又一村,我上厕所的时候突然灵光乍现、茅塞顿开,我居然忘掉了一个神器,一个可以完美解决我当前所面临的困境的神器。没错!这个神器就是 Redis !

🍊 别问我为什么可以在厕所里产生灵感,可能是因为厕所的环境可以让人更放松吧~ 有兴趣的小伙伴可以尝试一下下 😂

解决思路

🍑 实现思路: 既然 Session 出现了问题,那我索性就放弃掉它,也就是不使用 Session 来实现验证码的验证,而是将验证码转存到 Redis 中,存储验证码的时候还需要一个唯一标识(uuid)来作为 Key 值,前端在通过验证码接口获取验证码时,除了返回验证码本身以外,还要返回验证码的唯一标识(uuid),那么前端在提交表单的时候也要把 uuid 和用户输入的验证码一起传给后端进行验证。

P.S. 在之前的博客中说过关于 Redis 的整合以及 Redis 常用工具类,这里我就不再细说了。传送门:【大聪明教你学Java | Spring Boot 整合 Redis 实现访问量统计

我们既然已经想好了解决思路,接下来我们就一起看看具体代码👇

🥝 ① 首先还是要引入 Maven 依赖(我使用的是 Spring Boot 搭建的项目,这里就只贴一下除 Spring Boot 核心依赖以外的关键依赖)

<!--常用工具类 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<!-- 验证码 -->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>


<!-- redis 缓存操作 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- pool 对象池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- 阿里JSON解析器 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.80</version>
</dependency>

🥝 ② 验证码生成类(我这里返回的是验证码图片所对应的 Base64 编码,各位小伙伴要看清楚哦~)

/**
 * 生成验证码
 * @program: SysCaptchaController
 * @description: Controller
 * @author: 庄霸.liziye
 **/
@RestController
public class SysCaptchaController
{
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;

    /**
     * 生成验证码
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        ajax.put("captchaOnOff", "On");

        HttpSession session = request.getSession();

        // 保存验证码信息
        String uuid = IdUtils.simpleUUID();
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;

        String capStr = null, code = null;
        BufferedImage image = null;

        // 生成验证码
        String captchaType = request.getParameter("type");
        System.out.println("captchaType = " + captchaType);
        if ("math".equals(captchaType))
        {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }
        else if ("char".equals(captchaType))
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }

        //情况一:跨域时通过Redis存储验证码
        //通过Redis工具类来存储验证码的值,Key值为verifyKey,Value值为code 
        //此处省略一行代码


        //情况二:非跨域时通过session存储验证码
        session.setAttribute(verifyKey, code);

        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }

        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }
}

🥝 ③ 所需工具类(这里就只贴上图片转 Base64 编码的工具类,生成 uuid 的工具类网上有很多,这里就不贴了。如果想方便一些的话直接使用 random 方法随机生成一个即可;或者需要工具类的小伙伴可以在评论区留言,我把工具类发给你😀)

/**
 * Base64工具类
 * @program: Base64
 * @description: Base64
 * @author: 庄霸.liziye
 **/
public final class Base64
{
    static private final int     BASELENGTH           = 128;
    static private final int     LOOKUPLENGTH         = 64;
    static private final int     TWENTYFOURBITGROUP   = 24;
    static private final int     EIGHTBIT             = 8;
    static private final int     SIXTEENBIT           = 16;
    static private final int     FOURBYTE             = 4;
    static private final int     SIGN                 = -128;
    static private final char    PAD                  = '=';
    static final private byte[]  base64Alphabet       = new byte[BASELENGTH];
    static final private char[]  lookUpBase64Alphabet = new char[LOOKUPLENGTH];

    static
    {
        for (int i = 0; i < BASELENGTH; ++i)
        {
            base64Alphabet[i] = -1;
        }
        for (int i = 'Z'; i >= 'A'; i--)
        {
            base64Alphabet[i] = (byte) (i - 'A');
        }
        for (int i = 'z'; i >= 'a'; i--)
        {
            base64Alphabet[i] = (byte) (i - 'a' + 26);
        }

        for (int i = '9'; i >= '0'; i--)
        {
            base64Alphabet[i] = (byte) (i - '0' + 52);
        }

        base64Alphabet['+'] = 62;
        base64Alphabet['/'] = 63;

        for (int i = 0; i <= 25; i++)
        {
            lookUpBase64Alphabet[i] = (char) ('A' + i);
        }

        for (int i = 26, j = 0; i <= 51; i++, j++)
        {
            lookUpBase64Alphabet[i] = (char) ('a' + j);
        }

        for (int i = 52, j = 0; i <= 61; i++, j++)
        {
            lookUpBase64Alphabet[i] = (char) ('0' + j);
        }
        lookUpBase64Alphabet[62] = (char) '+';
        lookUpBase64Alphabet[63] = (char) '/';
    }

    private static boolean isWhiteSpace(char octect)
    {
        return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
    }

    private static boolean isPad(char octect)
    {
        return (octect == PAD);
    }

    private static boolean isData(char octect)
    {
        return (octect < BASELENGTH && base64Alphabet[octect] != -1);
    }

    /**
     * Encodes hex octects into Base64
     *
     * @param binaryData Array containing binaryData
     * @return Encoded Base64 array
     */
    public static String encode(byte[] binaryData)
    {
        if (binaryData == null)
        {
            return null;
        }

        int lengthDataBits = binaryData.length * EIGHTBIT;
        if (lengthDataBits == 0)
        {
            return "";
        }

        int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
        int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
        int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets;
        char encodedData[] = null;

        encodedData = new char[numberQuartet * 4];

        byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0;

        int encodedIndex = 0;
        int dataIndex = 0;

        for (int i = 0; i < numberTriplets; i++)
        {
            b1 = binaryData[dataIndex++];
            b2 = binaryData[dataIndex++];
            b3 = binaryData[dataIndex++];

            l = (byte) (b2 & 0x0f);
            k = (byte) (b1 & 0x03);

            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);
            byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc);

            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f];
        }

        // form integral number of 6-bit groups
        if (fewerThan24bits == EIGHTBIT)
        {
            b1 = binaryData[dataIndex];
            k = (byte) (b1 & 0x03);
            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4];
            encodedData[encodedIndex++] = PAD;
            encodedData[encodedIndex++] = PAD;
        }
        else if (fewerThan24bits == SIXTEENBIT)
        {
            b1 = binaryData[dataIndex];
            b2 = binaryData[dataIndex + 1];
            l = (byte) (b2 & 0x0f);
            k = (byte) (b1 & 0x03);

            byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0);
            byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0);

            encodedData[encodedIndex++] = lookUpBase64Alphabet[val1];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)];
            encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2];
            encodedData[encodedIndex++] = PAD;
        }
        return new String(encodedData);
    }

    /**
     * Decodes Base64 data into octects
     *
     * @param encoded string containing Base64 data
     * @return Array containind decoded data.
     */
    public static byte[] decode(String encoded)
    {
        if (encoded == null)
        {
            return null;
        }

        char[] base64Data = encoded.toCharArray();
        // remove white spaces
        int len = removeWhiteSpace(base64Data);

        if (len % FOURBYTE != 0)
        {
            return null;// should be divisible by four
        }

        int numberQuadruple = (len / FOURBYTE);

        if (numberQuadruple == 0)
        {
            return new byte[0];
        }

        byte decodedData[] = null;
        byte b1 = 0, b2 = 0, b3 = 0, b4 = 0;
        char d1 = 0, d2 = 0, d3 = 0, d4 = 0;

        int i = 0;
        int encodedIndex = 0;
        int dataIndex = 0;
        decodedData = new byte[(numberQuadruple) * 3];

        for (; i < numberQuadruple - 1; i++)
        {

            if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))
                    || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++])))
            {
                return null;
            } // if found "no data" just return null

            b1 = base64Alphabet[d1];
            b2 = base64Alphabet[d2];
            b3 = base64Alphabet[d3];
            b4 = base64Alphabet[d4];

            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
        }

        if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])))
        {
            return null;// if found "no data" just return null
        }

        b1 = base64Alphabet[d1];
        b2 = base64Alphabet[d2];

        d3 = base64Data[dataIndex++];
        d4 = base64Data[dataIndex++];
        if (!isData((d3)) || !isData((d4)))
        {// Check if they are PAD characters
            if (isPad(d3) && isPad(d4))
            {
                if ((b2 & 0xf) != 0)// last 4 bits should be zero
                {
                    return null;
                }
                byte[] tmp = new byte[i * 3 + 1];
                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
                tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
                return tmp;
            }
            else if (!isPad(d3) && isPad(d4))
            {
                b3 = base64Alphabet[d3];
                if ((b3 & 0x3) != 0)// last 2 bits should be zero
                {
                    return null;
                }
                byte[] tmp = new byte[i * 3 + 2];
                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
                tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
                tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
                return tmp;
            }
            else
            {
                return null;
            }
        }
        else
        { // No PAD e.g 3cQl
            b3 = base64Alphabet[d3];
            b4 = base64Alphabet[d4];
            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);

        }
        return decodedData;
    }

    /**
     * remove WhiteSpace from MIME containing encoded Base64 data.
     *
     * @param data the byte array of base64 data (with WS)
     * @return the new length
     */
    private static int removeWhiteSpace(char[] data)
    {
        if (data == null)
        {
            return 0;
        }

        // count characters that's not whitespace
        int newSize = 0;
        int len = data.length;
        for (int i = 0; i < len; i++)
        {
            if (!isWhiteSpace(data[i]))
            {
                data[newSize++] = data[i];
            }
        }
        return newSize;
    }
}

🥝 ④ 表单校验 Controller

@RequestMapping(value = "/add", method = RequestMethod.POST ,produces="application/json;charset=UTF-8,text/html;charset=UTF-8")
@ResponseBody
public AjaxResult add(HttpServletRequest request, HttpServletResponse response, @Validated FromInfo fromInfo) throws Exception {

    String verifyKey = Constants.CAPTCHA_CODE_KEY + fromInfo.getUuid();

	//情况一:跨域时,获取之前存储在Redis中的验证码
    //通过Redis工具类来存储验证码的值,Key值为verifyKey 
    //此处省略一行代码


    //情况二:非跨域时通过session存储验证码
    String captcha = (String) request.getSession().getAttribute(verifyKey);

    if (captcha == null){
        return AjaxResult.error("验证码为不能为空");
    }

    if (!fromInfo.getCaptacha().equalsIgnoreCase(captcha)){
        return AjaxResult.error("验证码有误,请重新核对");
    }

    /**
    * 此处写具体业务逻辑代码
    */

    return AjaxResult.success("提交成功");
}

🥝 ⑤ 前端获取验证码的Ajax方法及部分页面代码

/*点击刷新验证码*/
function changeCode() {
    $.ajax({
        url: '/captchaImage?type=char',
        type: 'get',
         success: function (msg) {
           document.getElementById('uuid').value = msg.uuid;
           $("#verifyCode").attr("src","data:image/png;base64,"+msg.img);
         }
    })
}

//隐藏表单存储uuid
<input type="hidden" name='uuid' value='' id='uuid' />

//验证码
<div class="easysite-row">
    <span class="easysite-label">验证码</span>
    <div class="easysite-cell">
        <input class="verifyInput" style="width: 100px;" type="text" id="verifyInput" name="captacha" />
        <img width="85%" id="verifyCode" class="verifyCode easysite-imgcode" onclick="changeCode()" />
    </div>
</div>

🍎 至此,因前后端分离导致无法从 Session 中拿到所需数据的问题就被我们完美的解决啦~ 本文提到的仅仅是若干种解决办法种的一种,不一定适用于所有的小伙伴。如果您有更好的解决办法,欢迎在评论区留言,不仅可以帮助其他小伙伴,还能为小弟指点一二 🌞

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●'◡'●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

你在被打击时,记起你的珍贵,抵抗恶意; 你在迷茫时,坚信你的珍贵,抛开蜚语; 爱你所爱 行你所行 听从你心 无问东西