SourceCode Java SpringBoot Linux Spring Code

无状态验证码工具类

Posted on 2022-03-22,6 min read
  • 项目中经常使用的验证码。
  • 通常我们校验验证码的时候会结合缓存(Redis或本地)来实现,为一个验证码唯一绑定一个UUID然后将验证码和UUID传送给前端,然后将UUID作为key,验证码的值作为value放入缓存,当前端传入验证码的时候,带着前端传过来的UUID去缓存里获取验证码的具体的值并做校验判断验证码是否正确。
  • 本文提供一种无状态验证码的实现方案(不引入三方验证码平台)

基本思路

  • 为了实现验证码的功能,并且减少系统的调用,验证码使用验证码 + 校验码的方式来进行验证。
  • 验证码校验码的格式为:验证码的用处_验证码内容_验证码有效期并且使用AES加密。为了保证安全,AES的密钥每次启动时,自动初始化。
    • 验证码的用处:用于标识验证码是用来做什么功能的,可以防止验证码跨场景使用(比如登录的验证码在注册的时候使用)。
    • 验证码内容:验证码具体的值,用于做比对。
    • 验证码有效期:一个时间戳,验证码最长有效时间,引入时间戳可以防止一个验证码一直被重复使用(时间戳内能重复使用),同时防止了生成验证码时,验证码内容重复导致的校验码重复。
  • 校验验证码的时候,则对验证码校验码进行解密,判断校验码的用处是否和当前接口的用处相符、判断验证码和校验码中的验证码是否符合、当前时间是否小于校验码的有效期。

相关依赖

  • 此处验证码绘制使用了easy-captcha依赖,而加密工具类使用了hutool

    <!--验证码相关依赖-->
    <dependency>
        <groupId>com.github.whvcse</groupId>
        <artifactId>easy-captcha</artifactId>
        <version>${captcha.version}</version>
    </dependency>
    <!--hutool依赖-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>${hutool.version}</version>
    </dependency>
    

代码

验证码实体

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 验证码实体类
 *
 * @author Gloduck
 * @date 2021/11/12
 */
@Data
@AllArgsConstructor
@ApiModel("验证码实体类")
public class VerifyCode {
    /**
     * 验证码图片
     */
    @ApiModelProperty("验证码图片")
    private String codeImage;
    /**
     * 用于校验的验证码
     */
    @ApiModelProperty("用于校验的验证码,有效期5分钟")
    private String verification;
}

验证码校验工具类

/**
 * 验证码工具
 *
 * @author Gloduck
 * @date 2021/11/12
 */
@Slf4j
@Component
public class VerifyCodeManager {
    /**
     * 验证码生成校验使用的密钥
     */
    private final byte[] VERIFY_CODE_KEY = SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded();


    /**
     * 验证码图片高
     */
    private static final int VERIFY_CODE_HEIGHT = 50;
    /**
     * 验证码图片宽
     */
    private static final int VERIFY_CODE_WIDTH = 150;


    /**
     * 验证码有效日期。5分钟
     */
    private static final int VERIFY_CODE_VALID_TIME = 5 * 60 * 1000;

    /**
     * 校验验证码
     *
     * @param usage        验证码的用处
     * @param verifyCode   验证码
     * @param verification 校验码
     * @return boolean
     */
    public boolean checkVerifyCode(String usage, String verifyCode, String verification) {
        if (usage == null || verifyCode == null || verification == null) {
            return false;
        }
        boolean isValid = true;
        try {
            SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, VERIFY_CODE_KEY);
            String decryptStr = aes.decryptStr(verification);
            log.debug("解密后的验证码内容为:{}", decryptStr);
            String[] split = decryptStr.split("_");
            // 如果校验码格式不正确,直接校验失败
            if (split.length != 3 || !NumberUtil.isNumber(split[2])) {
                isValid = false;
            }
            // 验证码的用处不符
            if (!split[0].equalsIgnoreCase(usage)) {
                isValid = false;
            }
            // 输入验证码和校验码不匹配
            if (!split[1].equalsIgnoreCase(verifyCode)) {
                isValid = false;
            }
            // 验证码超时
            if (System.currentTimeMillis() > Long.parseLong(split[2])) {
                isValid = false;
            }
        } catch (Exception e) {
            log.debug("验证码校验出错", e);
            isValid = false;
        }
        return isValid;
    }

    /**
     * 创建验证码
     * 因为此处是直接使用创建时的校验码对验证码进行校验,所以引入了时间来防止一个验证码一直使用的情况。
     * 每次重启应用,用于校验验证码的key会发生改变,导致验证码对应的验证的key失效。
     *
     * @param usage 验证码的用处
     * @return {@link VerifyCode}
     */
    public VerifyCode createVerifyCode(String usage) {
        // 生成验证码图片并获取验证码内容
        SpecCaptcha specCaptcha = new SpecCaptcha(VERIFY_CODE_WIDTH, VERIFY_CODE_HEIGHT);
        String base64Image = specCaptcha.toBase64();
        String code = specCaptcha.text();
        // 获取验证码有效期
        long expire = System.currentTimeMillis() + VERIFY_CODE_VALID_TIME;
        String verifyString = String.format("%s_%s_%d", usage, code, expire);
        // aes加密生成校验码
        SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, VERIFY_CODE_KEY);
        String verification = aes.encryptHex(verifyString);
        log.debug("生成的验证码为:{},用处为:{},有效期为:{}", code, usage, expire);
        return new VerifyCode(base64Image, verification);
    }
}

方案漏洞

  • 由于状态码是无状态的,所以此处会导致一个漏洞,就是一个验证码在有效期内,实际上是可以重复使用的,但是由于验证码一般是5min超时,所以这个验证码最多在5min内能无限使用,而且在正常访问的情况下,前端也会去刷新这个验证码,所以问题在可控范围内。

下一篇: ADB卸载MIUI系统软件→

loading...