滑块验证

540 阅读1分钟

引言

用户登录时增加滑块验证部分,进行图灵测试,实现人机识别,即区分真实用户和僵尸程序,一定程度上避免恶意操作。通过生成滑动验证码、装入分布式内存二级缓存、验证滑动验证码,完成整个滑块验证流程。

初始化分布式内存二级缓存

参考《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("验证码未通过验证");
    }
}

总结

综上,结合分布式缓存和限流提供了一种滑块验证方法,避免僵尸程序对系统的过度访问导致真实用户的服务可用性差。