1 验证码模块实现思路
- 首先检查是否启用了验证码功能,如果没有启用,则直接返回一个成功的AjaxResult对象;
- 生成一个唯一的UUID,并将其与验证码关联起来,以便后续验证;
- 根据系统配置的验证码类型(数学表达式或字符),生成相应的验证码和图片;
- 如果验证码类型是数学表达式,后端会随机生成一个固定的表达式比如(2*2=?@4),通过字符串符分割的方式,获得对应的题目和答案,后端会通过该题目生成对应的验证码图, 并将答案存放到redis中;
- 如果验证码类型是字符,后端会随机生成一个字符验证码文本,通过该文本生成对应的验证码图,并将验证码存放到redis中;
- 将生成的验证码图片转换为Base64编码的字符串,并将其放入AjaxResult对象中;
- 将UUID和Base64编码的验证码图片字符串返回给客户端;
- 前端通过调用后端api传入用户输入值和之前的key值,最终这个api查询redis,判断redis中的value是否和用户输入的值一样,最终实现验证码。
2 后端的实现步骤
验证码操作处理代码位于com.ruoyi.web.controller.common包下的CaptchaCotroller.java内
生成验证码的方法如下:
2.1 是否开启验证码模块
首先创建一个返回成功消息的AjaxResult对象;再检查是否启用验证码功能,将验证码启用状态添加到AjaxResult对象中;如果验证码功能未启用,则直接返回AjaxResult对象。
AjaxResult ajax = AjaxResult.success(); // 创建一个成功的AjaxResult对象
boolean captchaEnabled = configService.selectCaptchaEnabled(); // 检查是否启用了验证码功能
ajax.put("captchaEnabled", captchaEnabled); // 将验证码启用状态添加到AjaxResult对象中
if (!captchaEnabled)
{
return ajax; // 如果验证码功能未启用,则直接返回AjaxResult对象
}
按Ctrl键再点入selectCaptchaEnabled()函数,该函数的作用是根据配置来决定是否启用验证码功能。
/**
* 获取验证码开关
*
* @return true开启,false关闭
*/
@Override
public boolean selectCaptchaEnabled()
{
String captchaEnabled = selectConfigByKey("sys.account.captchaEnabled");
if (StringUtils.isEmpty(captchaEnabled))
{
return true;
}
return Convert.toBool(captchaEnabled);
}
其代码逻辑如下:
-
首先,它尝试从配置中获取一个名为"sys.account.captchaEnabled"的键值对。
-
如果这个键在配置中不存在(即StringUtils.isEmpty(captchaEnabled)返回true),那么默认启用验证码功能,方法返回true。
-
如果这个键在配置中存在,那么将获取到的字符串转换为布尔值并返回。这里使用了Convert.toBool方法,这个方法的具体实现没有给出,但从方法名来看,它可能是用来将字符串转换为布尔值的。
按Ctrl键再点入selectConfigByKey()函数,该方法的目的是为了从缓存或数据库中获取配置信息,并将其存入缓存以便下次使用。如果配置信息不存在,则返回一个空字符串。
/**
* 根据键名查询参数配置信息
*
* @param configKey 参数key
* @return 参数键值
*/
@Override
public String selectConfigByKey(String configKey)
{
String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
if (StringUtils.isNotEmpty(configValue))
{
return configValue;
}
SysConfig config = new SysConfig();
config.setConfigKey(configKey);
SysConfig retConfig = configMapper.selectConfig(config);
if (StringUtils.isNotNull(retConfig))
{
redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
return retConfig.getConfigValue();
}
return StringUtils.EMPTY;
}
其代码逻辑如下:
-
首先,它尝试从Redis缓存中获取名为configKey的配置信息。getCacheKey(configKey)方法用于生成缓存键,redisCache.getCacheObject(getCacheKey(configKey))从Redis缓存中获取对应的值。
-
如果缓存中存在配置信息,则将其转换为字符串并返回。StringUtils.isNotEmpty(configValue)检查转换后的字符串是否不为空。
-
如果缓存中不存在配置信息,则从数据库中查询。configMapper.selectConfig(config)方法执行数据库查询,并返回一个SysConfig对象。
-
如果数据库查询成功,且返回的SysConfig对象不为null,则将其配置值存入Redis缓存,并返回配置值。redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue())将配置值存入Redis缓存。
-
如果数据库中也不存在配置信息,则返回一个空字符串。StringUtils.EMPTY是一个表示空字符串的常量。
2.2 保存验证码信息
通过id生成器工具类生成一个唯一的UUID,并将UUID与验证码关联起来,这里生成的UUID就是Redis里的key
// 保存验证码信息
String uuid = IdUtils.simpleUUID(); // 通过id生成器工具类生成一个唯一的UUID
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; // 将UUID与验证码关联起来
String capStr = null, code = null;
BufferedImage image = null;
2.3 生成验证码
- 首先通过获取getCaptchaType()方法获取系统配置的验证码类型;
- 如果验证码类型为数学表达式,通过Google的验证码生成工具kaptcha的createText()方法生成数学表达式的验证码文本,该文本是一个类似于(2*2=?@4)的字符串,@符前面是题目,@符后面是对应的答案,通过字符串符分割的方式,获得对应的题目和答案,通过Google的验证码生成工具kaptcha的createImage()方法生成数学表达式的验证码图片;
- 如果验证码类型为字符,通过Google的验证码生成工具kaptcha的createText()方法生成字符验证码文本,再通过kaptcha的createImage()方法生成字符验证码图片;
- 将生成的验证码存入Redis缓存中,设置过期时间为Constants.CAPTCHA_EXPIRATION,时间单位是TimeUnit.MINUTES。
String captchaType = RuoYiConfig.getCaptchaType(); // 获取系统配置的验证码类型
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); // 生成字符验证码图片
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
2.4 转换成流写出信息
- 首先创建字节输出流FastByteArrayOutputStream,通过Java标准库里的ImageIO类的write()方法将验证码图片转换成字节流;
- 如果转换过程中出现异常,则返回一个包含错误信息的AjaxResult对象;
- 如果没有出现异常,将UUID和经过Base64编码的验证码图片字符串信息添加到AjaxResult对象并返回该对象。
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os); // 将验证码图片转换为字节流
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage()); // 如果转换过程中出现异常,则返回一个包含错误信息的AjaxResult对象
}
ajax.put("uuid", uuid); // 将UUID添加到AjaxResult对象中
ajax.put("img", Base64.encode(os.toByteArray())); // 将Base64编码的验证码图片字符串添加到AjaxResult对象中
return ajax; // 返回AjaxResult对象
3 前端的实现步骤
在页面初始化时调用自定义方法 getCode():
点击进入getCode()方法,该方法的主要作用是获取验证码图片,并将其显示在前端界面上,同时保存对应的key即UUID以便后续验证码的验证。
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (this.captchaEnabled) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
}
其代码逻辑如下:
- 调用getCodeImg方法,这个方法返回一个Promise,因此可以使用.then()来处理异步结果;
- 检查返回的响应中是否包含captchaEnabled字段,如果没有定义则默认为true;
- 如果验证码功能已启用,将返回的base64编码的图片数据转换为可以直接在前端显示的格式,将返回的uuid保存在组件的状态中,这个uuid可能用于后续的验证码验证.
自定义的getCodeImage的方法如下:
在该方法中调用了自定义方法request,该方法就是使用axios实现ajax
在该request.js中设置的了前端请求的共部分 VUE_APP_BASE_API, 这里该值为:
我们后续可以根据需求设置为上线环境:
上线环境的配置如下:
因为Request方法中设置了BaseURL所以对应的getCodeImg的请求路径为下:
我们通过观察可以发现前端地址是 localhost:80, 而我们的后端接口却是localhost:8080,存在跨域的问题,这种跨域问题前端和后端都有方法解决,这里通过前端解决此问题。
前端通过反向代理的方法解决,使用vue自带的反向代理服务器。
其中在进行反向代理的时候会将/dev-api重写成空,将localhost:80代理成localhost:8080,最终实现反向代理
最终前端实现验证码的效果。
4 参考链接
- 若依使用及源码解析(前后端分离版):https://blog.csdn.net/Ostkakah/article/details/132984838
- 若依框架学习(前后端分离)——(登录代码学习篇):http://t.csdnimg.cn/TAUyq