likes
comments
collection
share

若依源码分析--验证码生成过程

作者站长头像
站长
· 阅读数 20

往期文章:

若依作为最近非常火的脚手架,分析它的源码,不仅可以更好的使用它,在出错时及时定位,也可以在需要个性化功能时轻车熟路的修改它以满足我们自己的需求,同时也可以学习人家解决问题的思路,提升自己的技术水平

在若依的登录页面,需要输入账号、密码、验证码,这个验证码是一个简单的计算题。那么这个验证码是怎么生成的,里面有哪些小细节,我们使用若依的时候是不是可以修改这个验证码?

版本说明

以下源码内容是基于RuoYi-Vue-3.8.2版本,即前后端分离版本

总览

通过前端调用的url地址,找到生成验证码的方法CaptchaController.getCode,在ruoyi-admin包下。

整体流程:

  1. 项目初始化时, 将sys_config表中的数据存入redis,这是为了避免每次生成验证码都需要去mysql中查是否开启了验证码开关。
  2. 去redis中查看是否开启验证码,redis中没有则去mysql中查,都没有返回空字符串
  3. 如果不开启验证码配置,直接返回给前端
  4. 读取配置文件,获取生成验证码的方式(math/char)
  5. 将验证码的结果存储到redis中,key是前缀加UUID,过期时间2分钟
  6. 将验证码写入字节流中,再用Base64编码和UUID一起传给前端

代码如下,关键地方做了些注解

@RestController
public class CaptchaController
{
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

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

    @Autowired
    private RedisCache redisCache;
    
    @Autowired
    private ISysConfigService configService;
    /**
     * 生成验证码
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        // 查看验证码开关
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        // 如果不开启验证码配置,直接返回给前端
        if (!captchaEnabled)
        {
            return ajax;
        }

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

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

        // 生成验证码
        String captchaType = RuoYiConfig.getCaptchaType();  // 获取配置文件中验证码类型的配置
        if ("math".equals(captchaType))  // 计算器类型
        {
            String capText = captchaProducerMath.createText();
            // capText = 7-1=?@6
            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中,key是前缀+UUID, 过期时间2分钟
        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            // 写入流中
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }

        // 用base64编码把流转换一下,和uuid一起返回
        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }
}

关键点梳理

1. 配置验证码开关

在方法中,首先通过configService.selectCaptchaEnabled()查看验证码开关是否打开了,如果没有打开,直接返回。点进configService.selectCaptchaEnabled()方法中,我们发现,若依其实是先去redis中查找配置,找不到再去mysql中查找。

@Override
public String selectConfigByKey(String configKey)
{
    // 去redis里查看验证码开关配置
    String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
    if (StringUtils.isNotEmpty(configValue))
    {
        return configValue;
    }
    // 找不到去mysql里查
    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();
    }
    // mysql里也没有,返回空
    return StringUtils.EMPTY;
}

那为什么redis中存着配置呢,什么时候存的呢?

代码往上翻,在SysConfigServiceImpl.init方法,被注解@PostConstruct标注,原来在项目初始化时,将sys_config表中的数据存入redis

/**
 * 项目启动时,初始化参数到缓存
 */
@PostConstruct
public void init()
{
    loadingConfigCache();
}

2. 获取验证码类型

通过RuoYiConfig.getCaptchaType()方法去获取验证码类型。点进去发现他是配置类,前缀为(prefix = "ruoyi"),在application.yml中我们找到关于验证码类型的配置:

# 验证码类型 math 数字计算 char 字符验证
captchaType: math

我们可以在这里修改验证码的类型

  • math:为计算器验证码
  • char:字符类型验证码

3. math模式验证码生成过程

通过注入的bean,我们知道若依使用了google的kaptcha库用来生成验证码

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

我们找到这个配置类

@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath()
{
    DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
    Properties properties = new Properties();
    // 是否有边框 默认为true 我们可以自己设置yes,no
    properties.setProperty(KAPTCHA_BORDER, "yes");
    // 边框颜色 默认为Color.BLACK
    properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
    // 验证码文本字符颜色 默认为Color.BLACK
    properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
    // 验证码图片宽度 默认为200
    properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
    // 验证码图片高度 默认为50
    properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
    // 验证码文本字符大小 默认为40
    properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
    // KAPTCHA_SESSION_KEY
    properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
    // 验证码文本生成器
    properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator");
    // 验证码文本字符间距 默认为2
    properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
    // 验证码文本字符长度 默认为5
    properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
    // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
    properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
    // 验证码噪点颜色 默认为Color.BLACK
    properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
    // 干扰实现类
    properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
    // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
    properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
    Config config = new Config(properties);
    defaultKaptcha.setConfig(config);
    return defaultKaptcha;
}

注意这一行

// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator");

若依自己写了一个验证码文本生成器KaptchaTextCreator,继承了google的DefaultTextCreator类,重写了getText()方法。大家可以自行读取逻辑,最后生成的验证码格式为: 7-1=?@6

然后拆分为7-1=?6,将答案存入redis,有效期两分钟

4. 转换为流

通过kaptcha库的createImage方法,生成BufferedImage类型对象,将生成的图片写入到内存中,再通过ImageIO.write将文件以JPG格式写入到字节流中,这个输出字节流若依使用的是Spring提供的性能版ByteArrayOutputStreamFastByteArrayOutputStream。最后,再用base64编码把流转换一下,和uuid一起返回给前端

5. 前端代码

前端验证码相关的代码如下,在views.login

前端解析base64编码的方法:this.codeUrl = "data:image/gif;base64," + res.img;

<el-form-item prop="code" v-if="captchaEnabled">
  <el-input
    v-model="loginForm.code"
    auto-complete="off"
    placeholder="验证码"
    style="width: 63%"
    @keyup.enter.native="handleLogin"
  >
    <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
  </el-input>
  <div class="login-code">
    <img :src="codeUrl" @click="getCode" class="login-code-img"/>
  </div>
</el-form-item>
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;
    }
  });
},

总结

  1. 如果想在登录时,不需要输入验证码,可以在后台的参数设置中进行修改 若依源码分析--验证码生成过程
  2. 如果想修改验证码的类型,可以在配置文件application.yml中修改
  3. 我们甚至可以自己写一个验证码文本生成器,按照自己的逻辑生成一些奇形怪状的验证码,只要照着若依的写法写就可以