滑块验证码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实现

依赖

项目框架

在这里插入图片描述

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调接口的返回结果
    为了美观,base64部分做了删减,
    为了美观,base64部分做了删减,其中sliceImg为切好的小图的base64,bgImg为切图后背景图的base64。
  2. 前端页面的显示
    在这里插入图片描述

参考

https://www.jianshu.com/p/6ff29737209f?utm_campaign=haruk

  • 7
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
滑块验证码是一种常见的人机交互验证方式,主要用于防止恶意攻击和机器人攻击。下面是一个简单的Java实现滑块验证码的示例代码: 1. 首先,需要在前端页面上实现一个滑块组件,并在后台生成一个随机的验证码图片。 2. 然后,用户需要按住滑块并将其拖动到正确的位置,以验证自己是一个真正的人类用户。 3. 在后台,需要验证用户拖动滑块的位置是否正确,以确保用户通过了验证。 下面是一个基于Spring Boot框架的简单示例代码: 1. 在前端页面中添加如下代码: ```html <div class="slider-container"> <div class="slider-background"></div> <div class="slider-handle"></div> </div> ``` 2. 在后台代码中,需要生成一个随机的验证码图片,并将验证码信息保存在Session中,以便后续验证。以下是一个简单的验证码生成器示例代码: ```java import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; public class CaptchaGenerator { private static final int IMAGE_WIDTH = 200; private static final int IMAGE_HEIGHT = 80; private static final int LINE_COUNT = 20; private static final int CHAR_COUNT = 4; private static final String CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final int CHAR_SPACE = 20; private static final int CHAR_FONT_SIZE = 50; public static BufferedImage generate(String captcha) { BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT); g.setColor(Color.BLACK); Random random = new Random(); for (int i = 0; i < LINE_COUNT; i++) { int x1 = random.nextInt(IMAGE_WIDTH); int y1 = random.nextInt(IMAGE_HEIGHT); int x2 = random.nextInt(IMAGE_WIDTH); int y2 = random.nextInt(IMAGE_HEIGHT); g.drawLine(x1, y1, x2, y2); } Font font = new Font("Arial", Font.BOLD, CHAR_FONT_SIZE); g.setFont(font); int x = (IMAGE_WIDTH - CHAR_COUNT * CHAR_FONT_SIZE - (CHAR_COUNT - 1) * CHAR_SPACE) / 2; int y = (IMAGE_HEIGHT - CHAR_FONT_SIZE) / 2 + CHAR_FONT_SIZE; for (int i = 0; i < captcha.length(); i++) { char c = captcha.charAt(i); g.drawString(String.valueOf(c), x, y); x += CHAR_FONT_SIZE + CHAR_SPACE; } g.dispose(); return image; } public static String generateCaptcha() { Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < CHAR_COUNT; i++) { char c = CHAR_SET.charAt(random.nextInt(CHAR_SET.length())); sb.append(c); } return sb.toString(); } } ``` 3. 在Controller中,需要处理滑块验证请求,并进行验证码验证。以下是一个简单的Controller示例代码: ```java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.awt.image.BufferedImage; import java.io.IOException; @Controller public class CaptchaController { @GetMapping("/captcha") public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException { String captcha = CaptchaGenerator.generateCaptcha(); BufferedImage image = CaptchaGenerator.generate(captcha); HttpSession session = request.getSession(); session.setAttribute("captcha", captcha); response.setContentType("image/png"); response.getOutputStream().write(ImageUtil.toByteArray(image)); } @PostMapping("/captcha/verify") @ResponseBody public boolean verify(@RequestParam String captcha, HttpSession session) { String expectedCaptcha = (String) session.getAttribute("captcha"); return captcha.equals(expectedCaptcha); } } ``` 以上是一个简单的Java实现滑块验证码的示例代码。为了实现更好的安全性,实际应用中需要进一步加强验证机制,例如添加时间限制、IP限制等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值