🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。
🍊支持作者: 点赞👍、关注💖、留言💌~
这两天大聪明在开发一个小的应用软件,业务逻辑也很简单,其中涉及到一个需求:为了防止恶意提交表单数据,需要在表单中增加一个验证码。本来以为是一个挺简单的需求,但是今天却发现了不对劲的地方…
解决之旅正式开始 问题描述在提交表单的功能中,验证码从后台生成后返回到前端页面展示,同时为了实现验证码的校验,还需要在生成验证码时,将其存到 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 中拿到所需数据的问题就被我们完美的解决啦~ 本文提到的仅仅是若干种解决办法种的一种,不一定适用于所有的小伙伴。如果您有更好的解决办法,欢迎在评论区留言,不仅可以帮助其他小伙伴,还能为小弟指点一二 🌞
本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇
希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)
如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。
你在被打击时,记起你的珍贵,抵抗恶意;
你在迷茫时,坚信你的珍贵,抛开蜚语;
爱你所爱 行你所行 听从你心 无问东西
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)