pigXCloud滑块验证码右下角水印原理及配置

pigX框架登陆模块会有一个图片滑块验证码用于验证(利用com.github.anji-plus实现前后端验证码验证)。当用户填写完验证信息后点击登陆按钮即会弹出图片滑块验证信息,如下图:

	<dependency>
		<groupId>com.github.anji-plus</groupId>
		<artifactId>captcha-spring-boot-starter</artifactId>
		<version>1.2.4</version>
	</dependency>

不经想到这个验证码是怎么产生的,右下角的是水印还是图片信息的一部分呢?下面我们就来探究一下相关代码。

一、网关层

我们直观感受到的是通过code请求获取到的验证码信息。在pigX的网关中的RouterFunctionConfiguration类中可以看到会对code请求进行处理用于生成验证码信息。

@Bean
public RouterFunction routerFunction() {
   return RouterFunctions
         .route(RequestPredicates.path("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
               imageCodeCreateHandler)
         .andRoute(RequestPredicates.POST("/code/check").and(RequestPredicates.accept(MediaType.ALL)),
               imageCodeCheckHandler)
         .andRoute(RequestPredicates.GET("/swagger-resources").and(RequestPredicates.accept(MediaType.ALL)),
               swaggerResourceHandler)
         .andRoute(RequestPredicates.GET("/swagger-resources/configuration/ui")
               .and(RequestPredicates.accept(MediaType.ALL)), swaggerUiHandler)
         .andRoute(RequestPredicates.GET("/swagger-resources/configuration/security")
               .and(RequestPredicates.accept(MediaType.ALL)), swaggerSecurityHandler);

}

ImageCodeCreateHandler如下:

public Mono<ServerResponse> handle(ServerRequest serverRequest) {
   CaptchaVO vo = new CaptchaVO();
   vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
   CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
   ResponseModel responseModel = captchaService.get(vo);

   return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON)
         .body(BodyInserters.fromValue(objectMapper.writeValueAsString(R.ok(responseModel))));
}

CaptchaVO对象是缓存的图片验证码信息;ResponseModel是code请求的响应内容;captchaService.get(vo);方法即具体生成响应验证码的内容。

二、依赖配置相关内容

1、AjCaptchaServiceAutoConfiguration自动配置类

在AjCaptchaServiceAutoConfiguration配置类:

@Bean
@ConditionalOnMissingBean
public CaptchaService captchaService(AjCaptchaProperties prop) {
    logger.info("自定义配置项:{}", prop.toString());
    Properties config = new Properties();
    config.put(Const.CAPTCHA_CACHETYPE, prop.getCacheType().name());
    config.put(Const.CAPTCHA_WATER_MARK, prop.getWaterMark());
    config.put(Const.CAPTCHA_FONT_TYPE, prop.getFontType());
    config.put(Const.CAPTCHA_TYPE, prop.getType().getCodeValue());
    config.put(Const.CAPTCHA_INTERFERENCE_OPTIONS, prop.getInterferenceOptions());
    config.put(Const.ORIGINAL_PATH_JIGSAW, prop.getJigsaw());
    config.put(Const.ORIGINAL_PATH_PIC_CLICK, prop.getPicClick());
    config.put(Const.CAPTCHA_SLIP_OFFSET, prop.getSlipOffset());
    config.put(Const.CAPTCHA_AES_STATUS, prop.getAesStatus());
    config.put(Const.CAPTCHA_WATER_FONT, prop.getWaterFont());
    config.put(Const.CAPTCHA_CACAHE_MAX_NUMBER, prop.getCacheNumber());
    config.put(Const.CAPTCHA_TIMING_CLEAR_SECOND, prop.getTimingClear());
    if ((StringUtils.isNotBlank(prop.getJigsaw()) && prop.getJigsaw().startsWith("classpath:"))
            || (StringUtils.isNotBlank(prop.getPicClick()) && prop.getPicClick().startsWith("classpath:"))) {
        //自定义resources目录下初始化底图
        config.put(Const.CAPTCHA_INIT_ORIGINAL, "true");
        initializeBaseMap(prop.getJigsaw(), prop.getPicClick());
    }
    CaptchaService s = CaptchaServiceFactory.getInstance(config);
    return s;
}
@Override
public void init(Properties config) {
    //初始化底图
    boolean aBoolean = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_INIT_ORIGINAL));
    if (!aBoolean) {
        ImageUtils.cacheImage(config.getProperty(Const.ORIGINAL_PATH_JIGSAW),
                config.getProperty(Const.ORIGINAL_PATH_PIC_CLICK));
    }
    logger.info("--->>>初始化验证码底图<<<---");
    waterMark = config.getProperty(Const.CAPTCHA_WATER_MARK, "我的水印");
    slipOffset = config.getProperty(Const.CAPTCHA_SLIP_OFFSET, "5");
    waterMarkFont = config.getProperty(Const.CAPTCHA_WATER_FONT, "宋体");
    captchaAesStatus = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_AES_STATUS, "true"));
    fontType = config.getProperty(Const.CAPTCHA_FONT_TYPE, "宋体");
    cacheType = config.getProperty(Const.CAPTCHA_CACHETYPE, "local");
    captchaInterferenceOptions = Integer.parseInt(config.getProperty(Const.CAPTCHA_INTERFERENCE_OPTIONS, "0"));
    if (cacheType.equals("local")) {
        logger.info("初始化local缓存...");
        CacheUtil.init(Integer.parseInt(config.getProperty(Const.CAPTCHA_CACAHE_MAX_NUMBER, "1000")),
                Long.parseLong(config.getProperty(Const.CAPTCHA_TIMING_CLEAR_SECOND, "180")));
    }
}

CaptchaServiceFactory.getInstance(config)根据配置生成实例对象。可以看到在AbstractCaptchaService重写了init方法,并调用了ImageUtils工具类用于生成验证码信息,CAPTCHA_WATER_MARK属性,即右下角的内容是水印,且可以自定义配置,该属性通过"captcha.water.mark"进行配置。

@ConfigurationProperties(PREFIX)
public class AjCaptchaProperties {
    public static final String PREFIX = "aj.captcha";
    /**
     * 右下角水印文字(我的水印).
     */
    private String waterMark = "我的水印";
    //其他属性
    ...
    //toString()
}

可以看到还需要添加额外的前缀,因此在对应配置文件中的完整配信信息如下:

aj:
  captcha:
    water-mark: {myWaterMark}

验证码生成工具类,并附带相应注释:  

public class ImageUtils {
    private static Logger logger = LoggerFactory.getLogger(ImageUtils.class);
    private static Map<String, String> originalCacheMap = new ConcurrentHashMap();  //滑块底图
    private static Map<String, String> slidingBlockCacheMap = new ConcurrentHashMap(); //滑块拼图
    private static Map<String, String> picClickCacheMap = new ConcurrentHashMap(); //点选文字
    private static Map<String, String[]> fileNameMap = new ConcurrentHashMap<>();

    public static void cacheImage(String captchaOriginalPathJigsaw, String captchaOriginalPathClick) {
        //滑动拼图 即保存滑块验证码的位置,默认提供了6组
        if (StringUtils.isBlank(captchaOriginalPathJigsaw)) {
            originalCacheMap.putAll(getResourcesImagesFile("defaultImages/jigsaw/original"));
            slidingBlockCacheMap.putAll(getResourcesImagesFile("defaultImages/jigsaw/slidingBlock"));
        } else {
            originalCacheMap.putAll(getImagesFile(captchaOriginalPathJigsaw + File.separator + "original"));
            slidingBlockCacheMap.putAll(getImagesFile(captchaOriginalPathJigsaw + File.separator + "slidingBlock"));
        }
        //点选文字
        if (StringUtils.isBlank(captchaOriginalPathClick)) {
            picClickCacheMap.putAll(getResourcesImagesFile("defaultImages/pic-click"));
        } else {
            picClickCacheMap.putAll(getImagesFile(captchaOriginalPathClick));
        }
        fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
        fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
        fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
        logger.info("初始化底图:{}", JSONObject.toJSONString(fileNameMap));
    }

    public static void cacheBootImage(Map<String, String> originalMap, Map<String, String> slidingBlockMap, Map<String, String> picClickMap) {
        originalCacheMap.putAll(originalMap);
        slidingBlockCacheMap.putAll(slidingBlockMap);
        picClickCacheMap.putAll(picClickMap);
        fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
        fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
        fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
        logger.info("自定义resource底图:{}", JSONObject.toJSONString(fileNameMap));
    }


    public static BufferedImage getOriginal() {
        String[] strings = fileNameMap.get(CaptchaBaseMapEnum.ORIGINAL.getCodeValue());
        if (null == strings || strings.length == 0) {
            return null;
        }
        Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
        String s = originalCacheMap.get(strings[randomInt]);
        return getBase64StrToImage(s);
    }

    public static String getslidingBlock() {
        String[] strings = fileNameMap.get(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue());
        if (null == strings || strings.length == 0) {
            return null;
        }
        Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
        String s = slidingBlockCacheMap.get(strings[randomInt]);
        return s;
    }

    public static BufferedImage getPicClick() {
        String[] strings = fileNameMap.get(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue());
        if (null == strings || strings.length == 0) {
            return null;
        }
        Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
        String s = picClickCacheMap.get(strings[randomInt]);
        return getBase64StrToImage(s);
    }

    /**
     * 图片转base64 字符串
     *
     * @param templateImage
     * @return
     */
    public static String getImageToBase64Str(BufferedImage templateImage) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ImageIO.write(templateImage, "png", baos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        byte[] bytes = baos.toByteArray();

        Base64.Encoder encoder = Base64.getEncoder();

        return encoder.encodeToString(bytes).trim();
    }

    /**
     * base64 字符串转图片
     *
     * @param base64String
     * @return
     */
    public static BufferedImage getBase64StrToImage(String base64String) {
        try {
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] bytes = decoder.decode(base64String);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
            return ImageIO.read(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    private static Map<String, String> getResourcesImagesFile(String path) {
        //默认提供六张底图
        Map<String, String> imgMap = new HashMap<>();
        ClassLoader classLoader = ImageUtils.class.getClassLoader();
        for (int i = 1; i <= 6; i++) {
            InputStream resourceAsStream = classLoader.getResourceAsStream(path.concat("/").concat(String.valueOf(i).concat(".png")));
            byte[] bytes = new byte[0];
            try {
                bytes = FileCopyUtils.copyToByteArray(resourceAsStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
            String string = Base64Utils.encodeToString(bytes);
            String filename = String.valueOf(i).concat(".png");
            imgMap.put(filename, string);
        }
        return imgMap;
    }

    private static Map<String, String> getImagesFile(String path) {
        Map<String, String> imgMap = new HashMap<>();
        File file = new File(path);
        if (!file.exists()) {
            return new HashMap<>();
        }
        File[] files = file.listFiles();
        Arrays.stream(files).forEach(item -> {
            try {
                FileInputStream fileInputStream = new FileInputStream(item);
                byte[] bytes = FileCopyUtils.copyToByteArray(fileInputStream);
                String string = Base64Utils.encodeToString(bytes);
                imgMap.put(item.getName(), string);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        return imgMap;
    }

}

以上即提供的6组滑块验证码。

2、调用

在验证码生成处理器中

public Mono<ServerResponse> handle(ServerRequest serverRequest) {
   CaptchaVO vo = new CaptchaVO();
   vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
   CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
   ResponseModel responseModel = captchaService.get(vo);

   return ServerResponse.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON)
         .body(BodyInserters.fromValue(objectMapper.writeValueAsString(R.ok(responseModel))));
}

BlockPuzzleCaptchaServiceImpl继承了AbstractCaptchaService接口并重写了对应方法。其中,init()方法设定了水印的默认值,

public void init(Properties config) {
    //初始化底图
    boolean aBoolean = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_INIT_ORIGINAL));
    if (!aBoolean) {
        ImageUtils.cacheImage(config.getProperty(Const.ORIGINAL_PATH_JIGSAW),
                config.getProperty(Const.ORIGINAL_PATH_PIC_CLICK));
    }
    logger.info("--->>>初始化验证码底图<<<---");
    waterMark = config.getProperty(Const.CAPTCHA_WATER_MARK, "我的水印");
    slipOffset = config.getProperty(Const.CAPTCHA_SLIP_OFFSET, "5");
    waterMarkFont = config.getProperty(Const.CAPTCHA_WATER_FONT, "宋体");
    captchaAesStatus = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_AES_STATUS, "true"));
    fontType = config.getProperty(Const.CAPTCHA_FONT_TYPE, "宋体");
    cacheType = config.getProperty(Const.CAPTCHA_CACHETYPE, "local");
    captchaInterferenceOptions = Integer.parseInt(config.getProperty(Const.CAPTCHA_INTERFERENCE_OPTIONS, "0"));
    if (cacheType.equals("local")) {
        logger.info("初始化local缓存...");
        CacheUtil.init(Integer.parseInt(config.getProperty(Const.CAPTCHA_CACAHE_MAX_NUMBER, "1000")),
                Long.parseLong(config.getProperty(Const.CAPTCHA_TIMING_CLEAR_SECOND, "180")));
    }
}

通过captchaService.get()方法获取响应并返回,同时在get()方法中设定了右下角的水印信息。

public ResponseModel get(CaptchaVO captchaVO) {

    //原生图片
    BufferedImage originalImage = ImageUtils.getOriginal();
    if (null == originalImage) {
        logger.error("滑动底图未初始化成功,请检查路径");
        return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
    }
    //设置水印
    Graphics backgroundGraphics = originalImage.getGraphics();
    int width = originalImage.getWidth();
    int height = originalImage.getHeight();
    Font watermark = new Font(waterMarkFont, Font.BOLD, HAN_ZI_SIZE / 2);
    backgroundGraphics.setFont(watermark);
    backgroundGraphics.setColor(Color.white);
    backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7);

    //抠图图片
    String jigsawImageBase64 = ImageUtils.getslidingBlock();
    BufferedImage jigsawImage = ImageUtils.getBase64StrToImage(jigsawImageBase64);
    if (null == jigsawImage) {
        logger.error("滑动底图未初始化成功,请检查路径");
        return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
    }
    CaptchaVO captcha = pictureTemplatesCut(originalImage, jigsawImage, jigsawImageBase64);
    if (captcha == null
            || StringUtils.isBlank(captcha.getJigsawImageBase64())
            || StringUtils.isBlank(captcha.getOriginalImageBase64())) {
        return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_ERROR);
    }
    return ResponseModel.successData(captcha);
}

以上即通过anji-plus实现的滑块验证码信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值