引言
用户登录时增加滑块验证部分,进行图灵测试,实现人机识别,即区分真实用户和僵尸程序,一定程度上避免恶意操作。通过生成滑动验证码、装入分布式内存二级缓存、验证滑动验证码,完成整个滑块验证流程。
初始化分布式内存二级缓存
参考《redis 分布式缓存 springboot + redisson》基于 redisson 实现分布式内存二级缓存 Map,提供存储滑动验证码对象的缓存。
@PostConstruct
private void init() {
slideCaptchaCache = distributedLocalCacheManager.createCache(
RedisKeyUtils.getDistributedLocalCacheInstanceKey(CacheScene.SLIDE_CAPTCHA),
DistributedLocalCacheOption.builder()
.ttl(Duration.ofMinutes(1))
.distributedTtl(Duration.ofMinutes(2))
.build(),
StringKeyCodec.INSTANCE, new TypeReference<SlideCaptchaCache>(){}
);
}
生成滑动验证码
public SlideCaptcha generateSlideCaptcha() {
// 相关图片加载
LoadedImage backgroundImage = loadRandomBackgroundImage();
LoadedImage sliderTemplateImage = loadSliderTemplateImage(SLIDER_TEMPLATE_IMAGE_NAME);
LoadedImage sliderTemplateBorderImage = loadSliderTemplateImage(SLIDER_TEMPLATE_BORDER_IMAGE_NAME);
Pair<Integer, Integer> sliderCoordinates = calcSliderCoordinates(backgroundImage, sliderTemplateImage);
String id = UUID.randomUUID().toString().replace("-", "");
try (InputStream backgroundImageInputStream = backgroundImage.getImageResource().getInputStream()) {
// 1.根据坐标,选取裁切目标位置的背景图
Rectangle sliderBackgroundRectangle = new Rectangle(
sliderCoordinates.getLeft(), sliderCoordinates.getRight(),
sliderTemplateImage.getLoadedImage().getWidth(),
sliderTemplateImage.getLoadedImage().getHeight()
);
Iterator<ImageReader> imageReaderIterator = ImageIO.getImageReadersBySuffix(backgroundImage.getImageFileExtension());
if (!imageReaderIterator.hasNext()) {
log.error("生成滑动验证码失败");
}
ImageReader imageReader = imageReaderIterator.next();
imageReader.setInput(ImageIO.createImageInputStream(backgroundImageInputStream), true);
ImageReadParam imageReadParam = imageReader.getDefaultReadParam();
imageReadParam.setSourceRegion(sliderBackgroundRectangle);
BufferedImage sliderBackgroundImage = imageReader.read(0, imageReadParam);
// 2.根据裁切的背景图和滑块模板图绘制滑块图
// 根据模板图尺寸创建一张跟滑块图片相同大小的透明图片
BufferedImage sliderImage = new BufferedImage(
sliderTemplateImage.getLoadedImage().getWidth(),
sliderTemplateImage.getLoadedImage().getHeight(),
sliderTemplateImage.getLoadedImage().getType()
);
int[][] sliderTemplateImageMatrix = getImageMatrix(sliderTemplateImage.getLoadedImage());
// 将模板图非透明像素设置到滑块图中
for (int i = 0; i < sliderTemplateImageMatrix.length; i++) {
for (int j = 0; j < sliderTemplateImageMatrix[i].length; j++) {
if (sliderTemplateImageMatrix[i][j] < 0) {
sliderImage.setRGB(i, j, sliderBackgroundImage.getRGB(i, j));
}
}
}
// 滑块图描边
int[][] sliderTemplateBorderImageMatrix = getImageMatrix(sliderTemplateBorderImage.getLoadedImage());
for (int i = 0; i < sliderTemplateBorderImageMatrix.length; i++) {
for (int j = 0; j < sliderTemplateBorderImageMatrix[i].length; j++) {
if (sliderTemplateBorderImageMatrix[i][j] < 0) {
sliderImage.setRGB(i, j , -10);
}
}
}
// 图片绘图
Graphics2D graphics = sliderImage.createGraphics();
graphics.setBackground(Color.white);
// 设置抗锯齿属性
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setStroke(new BasicStroke(5, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
graphics.drawImage(sliderImage, 0, 0, null);
graphics.dispose();
// 3.根据滑块模板图和背景图生成验证码背景图
// 根据原图,创建支持 alpha 通道的 rgb 图片
BufferedImage argbBackgroundImage = new BufferedImage(
backgroundImage.getLoadedImage().getWidth(),
backgroundImage.getLoadedImage().getHeight(),
BufferedImage.TYPE_INT_ARGB
);
// 原图片矩阵
int[][] backgroundImageMatrix = getImageMatrix(backgroundImage.getLoadedImage());
// 将原图的像素拷贝到遮罩图
for (int i = 0; i < backgroundImageMatrix.length; i++) {
for (int j = 0; j < backgroundImageMatrix[i].length; j++) {
int rgb = backgroundImage.getLoadedImage().getRGB(i, j);
// 获取rgb色度
int r = (0xff & rgb);
int g = (0xff & (rgb >> 8));
int b = (0xff & (rgb >> 16));
// 无透明处理
rgb = r + (g << 8) + (b << 16) + (255 << 24);
argbBackgroundImage.setRGB(i, j, rgb);
}
}
// 对背景图根据滑块模板像素进行处理
for (int i = 0; i < sliderTemplateImageMatrix.length; i++) {
for (int j = 0; j < sliderTemplateImageMatrix[0].length; j++) {
int rgb = sliderTemplateImage.getLoadedImage().getRGB(i, j);
// 对源文件备份图像 (x+i, y+j) 坐标点进行透明处理
if (rgb < 0) {
int originRGB = argbBackgroundImage.getRGB(
sliderCoordinates.getLeft() + i, sliderCoordinates.getRight() + j
);
int r = (0xff & originRGB);
int g = (0xff & (originRGB >> 8));
int b = (0xff & (originRGB >> 16));
originRGB = r + (g << 8) + (b << 16) + (140 << 24);
// 对遮罩透明处理
argbBackgroundImage.setRGB(
sliderCoordinates.getLeft() + i,
sliderCoordinates.getRight() + j,
originRGB
);
}
}
}
ByteArrayOutputStream imageOutputStream = new ByteArrayOutputStream();
ImageIO.write(argbBackgroundImage, SLIDER_CAPTCHA_IMAGE_FORMAT, imageOutputStream);
ByteString backgroundImageByteString = ByteString.of(imageOutputStream.toByteArray());
imageOutputStream.flush();
imageOutputStream.reset();
ImageIO.write(sliderImage, SLIDER_CAPTCHA_IMAGE_FORMAT, imageOutputStream);
ByteString sliderImageByteString = ByteString.of(imageOutputStream.toByteArray());
imageOutputStream.flush();
imageOutputStream.reset();
// 缓存验证码信息
slideCaptchaCache.put(
id, SlideCaptchaCache.builder()
.id(id)
.x(sliderCoordinates.getLeft())
.y(sliderCoordinates.getRight())
.build()
);
return SlideCaptcha.builder()
.id(id)
.y(sliderCoordinates.getRight())
.sliderImageBase64(sliderImageByteString.base64())
.backgroundImageBase64(backgroundImageByteString.base64())
.build();
} catch (Exception e) {
log.error("生成滑动验证码失败");
}
}
验证滑动验证码
public void validateSlideCaptcha(SlideCaptchaCoordinate coordinate, boolean invalidate) {
SlideCaptchaCache captcha = slideCaptchaCache.get(coordinate.getId());
if (captcha == null) {
log.error("验证码已失效");
}
if (invalidate) {
slideCaptchaCache.invalidate(coordinate.getId());
}
if ( Math.abs(captcha.getX() - coordinate.getX()) >= SLIDE_CAPTCHA_COORDINATE_ALLOWED_ERROR
|| Math.abs(captcha.getY() - coordinate.getY()) >= SLIDE_CAPTCHA_COORDINATE_ALLOWED_ERROR) {
log.error("验证码未通过验证");
}
}
总结
综上,结合分布式缓存和限流提供了一种滑块验证方法,避免僵尸程序对系统的过度访问导致真实用户的服务可用性差。