滑块验证码Java实现

  • 滑块验证码的引入
  • 滑块验证码原理
  • 滑块验证码的Java实现
  • 说明
  • 依赖
  • 项目框架
  • java代码
  • 结果验证
  • 参考


滑块验证码的引入

最近滑动验证码在很多网站逐步流行起来,一方面对用户体验来说,比较新颖,操作简单,另一方面相对图形验证码来说,安全性并没有很大的降低。所以在项目中将登陆验证码方式改为滑块验证码。

滑块验证码原理

很多网站使用滑块验证码提高网站安全性,为了做到真正的验证,必须要走后台服务器。
下面是java实现滑块验证的核心步骤:

  1. 从服务器随机取一张图片,并对图片上的随机x,y坐标和宽高一块区域抠图;
  2. 根据步骤一的坐标和宽高,使用二维数组保存原图上抠图区域的像素点坐标;
  3. 根据步骤二的坐标点,对原图的抠图区域的颜色进行处理。
  4. 完成以上步骤之后得到两张图(扣下来的方块图,带有抠图区域阴影的原图),将这两张图和抠图区域的y坐标传到前台,前端在移动方块验证时,将移动后的x坐标传递到后台与原来的x坐标作比较,如果在阈值内则验证通过。
  5. 请求验证的步骤:前台向后台发起请求,后台随机一张图片做处理将处理完的两张图片的base64,抠图y坐标和token(token为后台缓存验证码的唯一token,可以用缓存和分布式缓存)返回给前台。
  6. 前台滑动图片将x坐标和token作为参数请求后台验证,服务器根据token取出x坐标与参数的x进行比较。

滑块验证码的Java实现

说明

项目是基于SpringBoot实现,前端是vue实现

依赖

项目框架

java滑块验证缺口计算 java实现滑块验证码_java

common包中存放验证码的常量;
controller包中存放验证码的controller类;
entity包中存放实体类;
tools包中存放验证码的工具类;

java代码

  • CaptchaConstant
public class CaptchaConstant {

    /**
     * key
     */
    public static final String TOKEN = "token";

    /**
     * 拼图所在x坐标名称
     */
    public static final String X = "x";

    /**
     * 拼图允许误差
     */
    public static final Integer SLICE_DIFF_LIMIT = 3;

    /**
     * redis最长存储时间5分钟
     */
    public static final int MINUTES_5 = 5;

}
  • ConfigConstant 拼图验证码配置
public class ConfigConstant {

    /**
     * 小图的宽 (SQUARE_W + CIRCLE_R * 2 + LIGHT * 2)
     */
    public static final int SMALL_IMG_W = 64;

    /**
     * 小图的高 (SQUARE_H + CIRCLE_R * 2 + LIGHT * 2)
     */
    public static final int SMALL_IMG_H = 64;

    /**
     * 正方形的宽
     */
    public static final int SQUARE_W = 40;

    /**
     * 正方形的高
     */
    public static final int SQUARE_H = 40;

    /**
     * 小图突出圆的直径 (CIRCLE_D * 2)
     */
    public static final int CIRCLE_D = 16;

    /**
     * 小图突出圆的半径
     */
    public static final int CIRCLE_R = 8;

    /**
     * 小图阴影宽度
     */
    public static final int SHADOW = 4;

    /**
     * 小图边缘高亮宽度
     */
    public static final int LIGHT = 4;

    /**
     * 小图边缘高亮颜色
     */
    public static final Color CIRCLE_GLOW_I_H = new Color(253, 239, 175, 148);
    public static final Color CIRCLE_GLOW_I_L = new Color(255, 209, 0);
    public static final Color CIRCLE_GLOW_O_H = new Color(253, 239, 175, 124);
    public static final Color CIRCLE_GLOW_O_L = new Color(255, 179, 0);

}
  • HDCaptchaController 拼图验证码controller类
@RestController
@RequestMapping("/hdcaptcha")
@Slf4j
public class HDCaptchaController {

    @Value("${xxx.path}")  // 在配置文件中配置原图存放路径
    private String captchaPath;

    @Autowired
    private RedisTool redisTool;


    /**
     * 注册验证码
     *
     * @param request
     * @return 验证码图片信息
     */
    @GetMapping("/register")
    public String register(HttpServletRequest request) {
        if (captchaPath == null) {
            ResultTool.Error(21);
        }

        Captcha captcha = new CaptchaUtil().createCaptcha(captchaPath);
        if (captcha == null) {
            ResultTool.Error(21);
        }

        // 生成注册token
        String token = captcha.getToken();
        // redis中保存x偏移量,保存时间5分钟
        redisTool.set(token, captcha.getX(), TimeUnit.MINUTES.toSeconds(CaptchaConstant.MINUTES_5));
        return ResultTool.Success(captcha);
    }

    /**
     * 拼图校验与登录
     *
     * @param request
     * @param form    拼图校验与登录信息
     * @return
     */
    @PostMapping("/check")
    public String check(HttpServletRequest request, @RequestBody CaptchaCheck form) {
        if (StringUtils.isBlank(form.getAccountName())) {
            return ResultToolUser.Error(13);
        }
        if (StringUtils.isBlank(form.getPassword())) {
            return ResultToolUser.Error(11);
        }
        if (StringUtils.isBlank(form.getToken())) {
            return ResultTool.Error(22);
        }
        if (null == form.getSliceX()) {
            return ResultTool.Error(23);
        }

        int x = (int) redisTool.get(form.getToken().trim());
        if (x < 0) {
            return ResultTool.Error(23);
        } else {
            int diff = x - form.getSliceX();
            if (diff < -CaptchaConstant.SLICE_DIFF_LIMIT || diff > CaptchaConstant.SLICE_DIFF_LIMIT) {
                return ResultTool.Error(23);
            }
        }
		// 验证码的验证和登录做了分离,若想做到一起可以在下边写登录的逻辑
        return ResultTool.Success("success!");
    }


}
  • Captcha 拼图验证码实体类
@Data
public class Captcha {

    /**
     * 滑动拼图块
     */
    private String sliceImg;

    /**
     * 背景图
     */
    private String bgImg;

    /**
     * 注册token
     */
    private String token;

    private Integer x;

    /**
     * 拼图所在y坐标
     */
    private Integer y;

}
  • CaptchaCheck 滑块验证码核验form
@Data
public class CaptchaCheck {

    /**
     * 登录名
     */
    private String accountName;

    /**
     * 登录密码
     */
    private String password;

    /**
     * 注册token
     */
    private String token;

    /**
     * 滑动x坐标
     */
    private Integer sliceX;

}
  • CaptchaUtil 滑块验证码的工具类
@Slf4j
public class CaptchaUtil {

    /**
     * 创建验证码
     *
     * @param captchaPath 验证码背景图保存路径
     * @return
     */
    public Captcha createCaptcha(String captchaPath) {
        File sourceFile = FileUtil.getSourceImage(captchaPath);
        // 原图图层
        BufferedImage sourceImg;
        try {
            sourceImg = ImageIO.read(sourceFile);
        } catch (IOException e) {
            log.error("读取验证码背景图出错", e);
            return null;
        }

        // 生成随机坐标
        Random random = new Random();
        // 滑动拼图x坐标范围为 [(0+40),(260-40)],y坐标范围为 [0,(160-40))
        int x = random.nextInt(sourceImg.getWidth() - 2 * ConfigConstant.SMALL_IMG_W) + ConfigConstant.SMALL_IMG_W;
        int y = random.nextInt(sourceImg.getHeight() - ConfigConstant.SMALL_IMG_H);
        log.info("滑动拼图坐标为({},{})", x, y);

        // 小图图层
        BufferedImage smallImg;
        try {
            smallImg = ImageUtil.cutSmallImg(sourceFile, x, y);
        } catch (IOException e) {
            log.error("创建验证码出错", e);
            return null;
        }
        // 创建shape区域
        List<Shape> shapes = createSmallShape();
        // 创建用于小图阴影和大图凹槽的图层
        List<BufferedImage> effectImgs = createEffectImg(shapes, smallImg);
        // 处理图片的边缘高亮及其阴影效果
        BufferedImage sliceImg = dealLightAndShadow(effectImgs.get(0), shapes.get(0));
        // 将灰色图当做水印印到原图上
        BufferedImage bgImg = ImageUtil.createBgImg(effectImgs.get(1), sourceImg, x, y);

        Captcha captchaDTO = new Captcha();
        captchaDTO.setBgImg(Base64Util.getImageBase64(bgImg, true));
        captchaDTO.setSliceImg(Base64Util.getImageBase64(sliceImg, false));
        captchaDTO.setX(x);
        captchaDTO.setY(y);
        captchaDTO.setToken(TokenUtil.createToken());
        return captchaDTO;
    }

    /**
     * 处理小图,在4个方向上随机找到2个方向添加凸出
     *
     * @return
     */
    private static List<Shape> createSmallShape() {
        int face1 = RandomUtils.nextInt(4);
        int face2;
        //使凸出1 与 凸出2不在同一个方向
        do {
            face2 = RandomUtils.nextInt(4);
        } while (face1 == face2);

        Shape shape1 = createShape(face1, 0);
        Shape shape2 = createShape(face2, 0);
        // 因为后边图形需要生成阴影,所以生成的小图shape + 阴影宽度 = 灰度化的背景小图shape(即大图上的凹槽)
        Shape bigShape1 = createShape(face1, ConfigConstant.SHADOW);
        Shape bigShape2 = createShape(face2, ConfigConstant.SHADOW);

        // 生成中间正方体Shape,(具体边界 + 弧半径 = x坐标位)
        int xStart = ConfigConstant.CIRCLE_R + ConfigConstant.LIGHT;
        int yStart = ConfigConstant.CIRCLE_R + ConfigConstant.LIGHT;
        Shape center = new Rectangle2D.Float(xStart, yStart, ConfigConstant.SQUARE_W, ConfigConstant.SQUARE_H);
        Shape bigCenter = new Rectangle2D.Float(xStart - (float) ConfigConstant.SHADOW / 2,
                yStart - (float) ConfigConstant.SHADOW / 2, ConfigConstant.SQUARE_W + ConfigConstant.SHADOW,
                ConfigConstant.SQUARE_H + ConfigConstant.SHADOW);

        // 合并Shape
        Area area = new Area(center);
        area.add(new Area(shape1));
        area.add(new Area(shape2));
        // 合并大Shape
        Area bigArea = new Area(bigCenter);
        bigArea.add(new Area(bigShape1));
        bigArea.add(new Area(bigShape2));

        List<Shape> list = new ArrayList<>();
        list.add(area);
        list.add(bigArea);
        return list;
    }

    /**
     * 创建圆形区域,半径为5
     * 由于小图边缘阴影的存在,坐标需加上此宽度
     *
     * @param type 0=上,1=左,2=下,3=右
     * @param size 圆外接矩形边长
     * @return
     */
    private static Shape createShape(int type, int size) {
        if (type < 0 || type > 3) {
            type = 0;
        }
        int x;
        int y;
        if (type == 0) {
            x = ConfigConstant.SQUARE_W / 2 + ConfigConstant.SHADOW;
            y = ConfigConstant.SHADOW;
        } else if (type == 1) {
            x = ConfigConstant.SHADOW;
            y = ConfigConstant.SQUARE_H / 2 + ConfigConstant.SHADOW;
        } else if (type == 2) {
            x = ConfigConstant.SQUARE_W / 2 + ConfigConstant.SHADOW;
            y = ConfigConstant.SQUARE_H + ConfigConstant.SHADOW;
        } else {
            x = ConfigConstant.SQUARE_W + ConfigConstant.SHADOW;
            y = ConfigConstant.SQUARE_H / 2 + ConfigConstant.SHADOW;
        }
        int halfSize = size / 2;
        int wSide = ConfigConstant.CIRCLE_D + size;
        return new Arc2D.Float(x - halfSize, y - halfSize, wSide, wSide, 90 * type, 190, Arc2D.CHORD);
    }

    /**
     * 创建用于小图阴影和大图凹槽的图层
     *
     * @param shapes
     * @param smallImg 小图原图
     * @return
     */
    private static List<BufferedImage> createEffectImg(List<Shape> shapes, BufferedImage smallImg) {
        Shape area = shapes.get(0);
        Shape bigArea = shapes.get(1);
        // 创建图层用于处理小图的阴影
        BufferedImage bfm1 = new BufferedImage(ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H,
                BufferedImage.TYPE_INT_ARGB);
        // 创建图层用于处理大图的凹槽
        BufferedImage bfm2 = new BufferedImage(ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H,
                BufferedImage.TYPE_INT_ARGB);
        for (int i = 0; i < ConfigConstant.SMALL_IMG_W; i++) {
            for (int j = 0; j < ConfigConstant.SMALL_IMG_W; j++) {
                if (area.contains(i, j)) {
                    bfm1.setRGB(i, j, smallImg.getRGB(i, j));
                }
                if (bigArea.contains(i, j)) {
                    bfm2.setRGB(i, j, Color.black.getRGB());
                }
            }
        }
        List<BufferedImage> list = new ArrayList<>();
        list.add(bfm1);
        list.add(bfm2);
        return list;
    }

    /**
     * 处理小图的边缘灯光及其阴影效果
     *
     * @param bfm
     * @param shape
     * @return
     */
    private static BufferedImage dealLightAndShadow(BufferedImage bfm, Shape shape) {
        //创建新的透明图层,该图层用于边缘化阴影, 将生成的小图合并到该图上
        BufferedImage buffimg = ((Graphics2D) bfm.getGraphics()).getDeviceConfiguration()
                .createCompatibleImage(ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H,
                        Transparency.TRANSLUCENT);
        Graphics2D graphics2d = buffimg.createGraphics();
        Graphics2D g2 = (Graphics2D) bfm.getGraphics();
        //原有小图,边缘亮色处理
        paintBorderGlow(g2, shape);
        //新图层添加阴影
        paintBorderShadow(graphics2d, shape);
        graphics2d.drawImage(bfm, 0, 0, null);
        return buffimg;
    }

    /**
     * 处理边缘亮色
     *
     * @param g2
     * @param clipShape
     */
    private static void paintBorderGlow(Graphics2D g2, Shape clipShape) {
        int gw = ConfigConstant.LIGHT * 2;
        for (int i = gw; i >= 2; i -= 2) {
            float pct = (float) (gw - i) / (gw - 1);
            Color mixHi = getMixedColor(ConfigConstant.CIRCLE_GLOW_I_H, pct, ConfigConstant.CIRCLE_GLOW_O_H,
                    1.0f - pct);
            Color mixLo = getMixedColor(ConfigConstant.CIRCLE_GLOW_I_L, pct, ConfigConstant.CIRCLE_GLOW_O_L,
                    1.0f - pct);
            g2.setPaint(new GradientPaint(0.0f, 35 * 0.25f, mixHi, 0.0f, 35, mixLo));
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
            g2.setStroke(new BasicStroke(i));
            g2.draw(clipShape);
        }
    }

    /**
     * 处理阴影
     *
     * @param g1
     * @param clipShape
     */
    private static void paintBorderShadow(Graphics2D g1, Shape clipShape) {
        g1.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        int sw = ConfigConstant.SHADOW * 2;
        for (int i = sw; i >= 2; i -= 2) {
            float pct = (float) (sw - i) / (sw - 1);
            //pct<03. 用于去掉阴影边缘白边,  pct>0.8用于去掉过深的色彩, 如果使用Color.lightGray. 可去掉pct>0.8
            if (pct < 0.3 || pct > 0.8) {
                continue;
            }
            g1.setColor(getMixedColor(new Color(54, 54, 54), pct, Color.WHITE, 1.0f - pct));
            g1.setStroke(new BasicStroke(i));
            g1.draw(clipShape);
        }
    }

    private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
        float[] clr1 = c1.getComponents(null);
        float[] clr2 = c2.getComponents(null);
        for (int i = 0; i < clr1.length; i++) {
            clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
        }
        return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
    }

}
  • ImageUtil 图片工具类
class ImageUtil {

    /**
     * 创建小块拼图
     *
     * @param file 背景原图
     * @param x    小块拼图x坐标
     * @param y    小块拼图y坐标
     * @return
     */
    static BufferedImage cutSmallImg(File file, int x, int y) throws IOException {
        Iterator<ImageReader> iterator = ImageIO.getImageReadersByFormatName("png");
        ImageReader render = iterator.next();
        ImageInputStream in = ImageIO.createImageInputStream(new FileInputStream(file));
        render.setInput(in, true);
        BufferedImage bufferedImage;
        try {
            ImageReadParam param = render.getDefaultReadParam();
            Rectangle rect = new Rectangle(x, y, ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H);
            param.setSourceRegion(rect);
            bufferedImage = render.read(0, param);
        } finally {
            if (in != null) {
                in.close();
            }
        }
        return bufferedImage;
    }

    /**
     * 创建一个灰度化图层, 将生成的小图,覆盖到该图层,使其灰度化,用于作为一个水印图
     *
     * @param smallImage 小图
     * @param originImg  原图
     * @param x          x坐标
     * @param y          y坐标
     * @return
     */
    static BufferedImage createBgImg(BufferedImage smallImage, BufferedImage originImg, int x, int y) {
        // 将灰度化之后的图片,整合到原有图片上
        Graphics2D graphics2d = originImg.createGraphics();
        graphics2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.6F));
        graphics2d.drawImage(smallImage, x, y, null);
        // 释放
        graphics2d.dispose();
        return originImg;
    }

    /**
     * 压缩图片
     *
     * @param originImg
     * @return
     */
    static byte[] compressImg(BufferedImage originImg) {
        ImageWriter imageWriter = null;
        ByteArrayOutputStream outputStream = null;
        try {
            int width = originImg.getWidth();
            int height = originImg.getHeight();
            BufferedImage newBufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB);
            Graphics2D graphics2d = newBufferedImage.createGraphics();
//            graphics2D.setBackground(new Color(255, 255, 255));
            graphics2d.clearRect(0, 0, width, height);
            graphics2d.drawImage(originImg.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);

            imageWriter = ImageIO.getImageWritersByFormatName("png").next();
            outputStream = new ByteArrayOutputStream();
            imageWriter.setOutput(ImageIO.createImageOutputStream(outputStream));
            imageWriter.write(new IIOImage(newBufferedImage, null, null));
            outputStream.flush();
            return outputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (imageWriter != null) {
                imageWriter.abort();
            }
            IOUtils.closeQuietly(outputStream);
        }
    }

}

其他的工具类这里不再给出。

结果验证
  1. PostMan调接口的返回结果
  2. java滑块验证缺口计算 java实现滑块验证码_java_02

  3. 为了美观,base64部分做了删减,其中sliceImg为切好的小图的base64,bgImg为切图后背景图的base64。
  4. 前端页面的显示
  5. java滑块验证缺口计算 java实现滑块验证码_spring boot_03