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实现的滑块验证码信息。