(04)Dubbo微服务实战-图形验证码服务

1,314 阅读3分钟

图形验证码服务

图形验证码多用于登录验证,或其他重要资源验证,防止机器破解。由于图形验证码需要在项目中经常使用,而且不属于具体的业务,因此把此服务单独抽取出来。

该服务编写了创建图形验证码的接口以及检验图形验证码的接口,创建时会把验证码进行缓存,校验时会删除验证码,验证码需要设置过期时间。

使用技术:Redis+Base64

生成图形验证码的工具类

该工具主要用于生成图形验证码,可以指定宽度,长度和验证码长度。返回的内容是Base64编码的图形验证码和验证码字符串。

实现代码

/**
 * 描述:生成图形验证码的工具类
 *
 * @author: xhsf
 * @create: 2020/11/22 16:14
 */
public class ImageAuthCodeUtils {

    /**
     * 该方法用于生成图形验证码
     *
     * @param width 验证码宽度
     * @param height 验证码高度
     * @param length 验证码字符串长度
     * @return 验证码图片的Base64编码和验证码
     */
    public static ImageAuthCode createImageCode(int width, int height, int length) {
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        Random random = new Random();
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }
        g.dispose();

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpeg", stream);
        }
        // 不会抛出异常
        catch (IOException ignored) {
        }
        String base64Image = Base64.encode(stream.toByteArray());

        return new ImageAuthCode(base64Image, sRand.toString());
    }

    private static Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    public static class ImageAuthCode {

        /**
         * base64形式编码的图形验证码
         */
        private String base64Image;

        /**
         * 图形验证码里面的验证码字符串
         */
        private String authCode;
        
        // getters and setters
    }
}

依赖的库

        <!--    spring-social-config用户生成图形验证码    -->
        <dependency>
            <groupId>org.springframework.social</groupId>
            <artifactId>spring-social-config</artifactId>
        </dependency>

        <!--    用于转base64    -->
        <dependency>
            <groupId>org.apache.axis</groupId>
            <artifactId>axis</artifactId>
        </dependency>

封装成服务

创建图形验证码

这里主要是创建验证码,并对验证码进行缓存。这里验证码的编号使用UUID+自增id,自增id使用到了Redis的lua脚本生成。

    /**
     * 创建图形验证码
     * 会把验证码缓存,可用通过checkImageAuthCode检查是否通过校验
     *
     * @param imageAuthCodeDTO ImageAuthCodeDTO
     * @return ImageAuthCodeDTO
     */
    @Override
    public Result<ImageAuthCodeDTO> createImageAuthCode(ImageAuthCodeDTO imageAuthCodeDTO) {
        // 创建图形验证码
        ImageAuthCodeUtils.ImageAuthCode imageAuthCode = ImageAuthCodeUtils.createImageCode(
                imageAuthCodeDTO.getWidth(), imageAuthCodeDTO.getHeight(), imageAuthCodeDTO.getLength());

        // 创建图形验证码编号
        Long incrementId = redisTemplate.execute(incrementIdRedisScript,
                Collections.singletonList(INCREMENT_ID_REDIS_KEY_PREFIX), "0");
        String id = UUID.randomUUID().toString() + incrementId;

        // 添加验证码到缓存
        String redisKey = IMAGE_AUTH_CODE_REDIS_KEY_PREFIX + ":" + id;
        redisTemplate.opsForValue().set(redisKey, imageAuthCode.getAuthCode());
        redisTemplate.expire(redisKey, imageAuthCodeDTO.getExpiredTime(), TimeUnit.MINUTES);

        // 构造并返回
        return Result.success(new ImageAuthCodeDTO.Builder()
                .id(id)
                .authCode(imageAuthCode.getBase64Image())
                .build());
    }

lua脚本

local id = redis.call('GET', KEYS[1])
if not id then
    redis.call('SET', KEYS[1], ARGV[1])
end
return redis.call('INCR', KEYS[1])

检验图形验证码

    /**
     * 校验验证码
     * 会从缓存读取验证码进行校验
     * 该接口不管校验是否通过都会删除缓存里的验证码
     * 即验证码只能进行一次校验(进行一次校验后即失效)
     *
     * @param id 图形验证码编号
     * @param authCode 用户输入的验证码字符串
     * @return 校验结果
     */
    @Override
    public Result<Void> checkImageAuthCode(String id, String authCode) {
        // 从缓存获取图形验证码并删除
        String redisKey = IMAGE_AUTH_CODE_REDIS_KEY_PREFIX + ":" + id;
        String authCodeInRedis = (String) redisTemplate.opsForValue().get(redisKey);
        redisTemplate.delete(redisKey);

        // 判断验证码是否相同
        if (!authCode.equals(authCodeInRedis)) {
            return Result.fail(ErrorCode.INVALID_PARAMETER);
        }
        return Result.success();
    }

关于微信小程序使用Base64的坑

在微信小程序中直接嵌入上面服务所生成的二维码时发现图片并不能够显示,通过查阅资料后知道需要把Base64字符串中的回车换行替换成空字符''。使用如下代码转换。

base64Image.replace(/[\r\n]/g, '') // 将回车换行换为空字符''  

其他代码

ImageAuthCodeDTO

图形验证码传输类型

/**
 * 描述:图形验证码传输类型
 *
 * @author: xhsf
 * @create: 2020/11/22 15:44
 */
public class ImageAuthCodeDTO implements Serializable {
    /**
     * 唯一标识一个图形验证码
     * 格式为:随机字符串+序列编号
     */
    private String id;

    /**
     * 图形验证码的Base64编码字符串
     */
    private String authCode;

    /**
     * 图形验证码宽度
     */
    @NotNull(groups = ImageAuthCodeService.CreateImageAuthCode.class)
    @Positive
    private Integer width;

    /**
     * 图形验证码高度
     */
    @NotNull(groups = ImageAuthCodeService.CreateImageAuthCode.class)
    @Positive
    private Integer height;

    /**
     * 验证码长度(位数)
     */
    @NotNull(groups = ImageAuthCodeService.CreateImageAuthCode.class)
    @Positive
    @Max(10)
    private Integer length;

    /**
     * 图形验证码有效时间,单位分钟
     */
    @NotNull(groups = ImageAuthCodeService.CreateImageAuthCode.class)
    @Positive
    @Max(10)
    private Integer expiredTime;
}