java和vue实现滑动拼图验证码

  • 前言
  • 效果图
  • 正文
    • 后端Java
      • 依赖
      • 核心代码
      • Controller
    • 前端vue
  • 最后成品
  • 扩展GIf动态拼图
  • 引用

前言

由于最近想做一个滑块验证码,苦于后端没有精美好看的样式,纯前端的验证码倒是挺好看的,但是不安全啊。找了很久一直没有找到想要的效果。无奈只能自己尝试着来干了。
网络上很多java生成拼图验证码的代码。但是都不太灵活,下面我们来看下。

效果图



这里要感谢作者javaLuo[^1]开源的vue-puzzle-vcode,我们的前端样式基本上基于该开源代码来修改。

正文

后端Java

依赖

<!-- 图像处理滤镜,必要 -->
<dependency><groupId>com.jhlabs</groupId><artifactId>filters</artifactId><version>2.0.235</version>
</dependency>
<!--非必要,工具类的集合-->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.4.1</version>
</dependency>
<!--lombok,非必要,主要就是生成set和get方法-->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>

核心代码

主要技术点
1.使用GeneralPath描绘选区,这里区别其他文章方式。
2.使用jhlabs滤镜库来做滑块的阴影,主图的内阴影,做出立体的效果来
新增了描边功能

import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.util.NumberUtil;
import com.jhlabs.image.ImageUtils;
import com.jhlabs.image.InvertAlphaFilter;
import com.jhlabs.image.ShadowFilter;
import lombok.Data;import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.util.Random;/*** 滑块验证码** @author hexm* @date 2020/10/23*/
@Data
public class PuzzleCaptcha {/** 默认宽度,用来计算阴影基本长度 */private static final int DEFAULT_WIDTH = 280;/** 随机数 */private static final Random RANDOM = new Random();/** 蒙版 */private static Color color = new Color(255, 255, 255, 204);/** alpha通道过滤器 */private static InvertAlphaFilter alphaFilter = new InvertAlphaFilter();/** 边距 */private static int margin = 10;/** 生成图片的宽度 */private int width = DEFAULT_WIDTH;/** 生成图片高度 */private int height = 150;/** x轴的坐标,由算法决定 */private int x;/** y轴的坐标,由算法决定 */private int y;/** 拼图长宽 */private int vwh = 10 * 3;/** 原图 */private Image image;/** 大图 */private Image artwork;/** 小图 */private Image vacancy;/** 是否注重速度 */private boolean isFast = false;/** 小图描边颜色 */private Color vacancyBorderColor;/** 小图描边线条的宽度 */private float vacancyBorderWidth = 2.5f;/** 主图描边的颜色 */private Color artworkBorderColor;/** 主图描边线条的宽度 */private float artworkBorderWidth = 5f;/*** 最高放大倍数,合理的放大倍数可以使图像平滑且提高渲染速度* 当isFast为false时,此属性生效* 放大倍数越高,生成的图像越平滑,受原始图片大小的影响。*/private double maxRatio = 2;/*** 画质** @see Image#SCALE_DEFAULT* @see Image#SCALE_FAST* @see Image#SCALE_SMOOTH* @see Image#SCALE_REPLICATE* @see Image#SCALE_AREA_AVERAGING*/private int imageQuality = Image.SCALE_SMOOTH;/*** 从文件中读取图片** @param file*/public PuzzleCaptcha(File file) {image = ImgUtil.read(file);}/*** 从文件中读取图片,请使用绝对路径,使用相对路径会相对于ClassPath** @param imageFilePath*/public PuzzleCaptcha(String imageFilePath) {image = ImgUtil.read(imageFilePath);}/*** 从{@link Resource}中读取图片** @param resource*/public PuzzleCaptcha(Resource resource) {image = ImgUtil.read(resource);}/*** 从流中读取图片** @param imageStream*/public PuzzleCaptcha(InputStream imageStream) {image = ImgUtil.read(imageStream);}/*** 从图片流中读取图片** @param imageStream*/public PuzzleCaptcha(ImageInputStream imageStream) {image = ImgUtil.read(imageStream);}/*** 加载图片** @param image*/public PuzzleCaptcha(Image image) {this.image = image;}/*** 加载图片** @param bytes*/public PuzzleCaptcha(byte[] bytes) {this.image = ImgUtil.read(new ByteArrayInputStream(bytes));}/*** 生成随机x、y坐标*/private void init() {if (x == 0 || y == 0) {this.x = random(vwh, this.width - vwh - margin);this.y = random(margin, this.height - vwh - margin);}}/*** 执行*/public void run() {init();// 缩略图Image thumbnail;GeneralPath path;int realW = image.getWidth(null);int realH = image.getHeight(null);int w = realW, h = realH;double wScale = 1, hScale = 1;// 如果原始图片比执行的图片还小,则先拉伸再裁剪boolean isFast = this.isFast || w < this.width || h < this.height;if (isFast) {// 缩放,使用平滑模式thumbnail = image.getScaledInstance(width, height, imageQuality);path = paintBrick(1, 1);w = this.width;h = this.height;} else {// 缩小到一定的宽高,保证裁剪的圆润boolean flag = false;if (realW > width * maxRatio) {// 不超过最大倍数且不超过原始图片的宽w = Math.min((int) (width * maxRatio), realW);flag = true;}if (realH > height * maxRatio) {h = Math.min((int) (height * maxRatio), realH);flag = true;}if (flag) {// 若放大倍数生效,则缩小图片至最高放大倍数,再进行裁剪thumbnail = image.getScaledInstance(w, h, imageQuality);} else {thumbnail = image;}hScale = NumberUtil.div(h, height);wScale = NumberUtil.div(w, width);path = paintBrick(wScale, hScale);}// 创建阴影过滤器float radius = 5 * ((float) w / DEFAULT_WIDTH) * (float) wScale;int left = 1;ShadowFilter shadowFilter = new ShadowFilter(radius, 2 * (float) wScale, -1 * (float) hScale, 0.8f);// 创建空白的图片BufferedImage artwork = translucent(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB));BufferedImage localVacancy = translucent(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB));// 画小图Graphics2D vg = localVacancy.createGraphics();// 抗锯齿vg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 设置画图路径范围vg.setClip(path);// 将区域中的图像画到小图中vg.drawImage(thumbnail, null, null);//描边if (vacancyBorderColor != null) {vg.setColor(vacancyBorderColor);vg.setStroke(new BasicStroke(vacancyBorderWidth));vg.draw(path);}// 释放图像vg.dispose();// 画大图// 创建画笔Graphics2D g = artwork.createGraphics();// 抗锯齿g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 画上图片g.drawImage(thumbnail, null, null);// 设置画图路径范围g.setClip(path);// 填充缺口透明度 颜色混合,不透明在上g.setComposite(AlphaComposite.SrcAtop);// 填充一层白色的透明蒙版,透明度越高,白色越深 alpha:0-255g.setColor(color);g.fill(path);//描边if (artworkBorderColor != null) {g.setColor(artworkBorderColor);g.setStroke(new BasicStroke(artworkBorderWidth));g.draw(path);}// 画上基于小图的内阴影,先反转alpha通道,然后创建阴影g.drawImage(shadowFilter.filter(alphaFilter.filter(localVacancy, null), null), null, null);// 释放图像g.dispose();// 裁剪掉多余的透明背景localVacancy = ImageUtils.getSubimage(localVacancy, (int) (x * wScale - left), 0, (int) Math.ceil(path.getBounds().getWidth() + radius) + left, h);if (isFast) {// 添加阴影this.vacancy = shadowFilter.filter(localVacancy, null);this.artwork = artwork;} else {// 小图添加阴影localVacancy = shadowFilter.filter(localVacancy, null);// 大图缩放this.artwork = artwork.getScaledInstance(width, height, imageQuality);// 缩放时,需要加上阴影的宽度,再除以放大比例this.vacancy = localVacancy.getScaledInstance((int) ((path.getBounds().getWidth() + radius) / wScale), height, imageQuality);}}/*** 绘制拼图块的路径** @param xScale x轴放大比例* @param yScale y轴放大比例* @return*/private GeneralPath paintBrick(double xScale, double yScale) {double x = this.x * xScale;double y = this.y * yScale;// 直线移动的基础距离double hMoveL = vwh / 3f * yScale;double wMoveL = vwh / 3f * xScale;GeneralPath path = new GeneralPath();path.moveTo(x, y);path.lineTo(x + wMoveL, y);// 上面的圆弧正东方向0°,顺时针负数,逆时针正数path.append(arc(x + wMoveL, y - hMoveL / 2, wMoveL, hMoveL, 180, -180), true);path.lineTo(x + wMoveL * 3, y);path.lineTo(x + wMoveL * 3, y + hMoveL);// 右边的圆弧path.append(arc(x + wMoveL * 2 + wMoveL / 2, y + hMoveL, wMoveL, hMoveL, 90, -180), true);path.lineTo(x + wMoveL * 3, y + hMoveL * 3);path.lineTo(x, y + hMoveL * 3);path.lineTo(x, y + hMoveL * 2);// 左边的内圆弧path.append(arc(x - wMoveL / 2, y + hMoveL, wMoveL, hMoveL, -90, 180), true);path.lineTo(x, y);path.closePath();return path;}/*** 绘制圆形、圆弧或者是椭圆形* 正东方向0°,顺时针负数,逆时针正数** @param x      左上角的x坐标* @param y      左上角的y坐标* @param w      宽* @param h      高* @param start  开始的角度* @param extent 结束的角度* @return*/private Arc2D arc(double x, double y, double w, double h, double start, double extent) {return new Arc2D.Double(x, y, w, h, start, extent, Arc2D.OPEN);}/*** 透明背景** @param bufferedImage* @return*/private BufferedImage translucent(BufferedImage bufferedImage) {Graphics2D g = bufferedImage.createGraphics();bufferedImage = g.getDeviceConfiguration().createCompatibleImage(bufferedImage.getWidth(), bufferedImage.getHeight(), Transparency.TRANSLUCENT);g.dispose();return bufferedImage;}/*** 随机数** @param min* @param max* @return*/private static int random(int min, int max) {return RANDOM.ints(min, max + 1).findFirst().getAsInt();}
}

注释很详细了,就不多描述了。

Controller

技术要点
1.生成验证码图片,将宽度、当前时间戳与偏移量x存入缓存redis中。
2.验证前端的宽度与原始图片的宽度比例。比较偏移量x的时候需要乘以这个比例,否则会出现后端图片大,前端图片小,偏移量和后端差距过大不通过。
3.与生成照片的时间戳比较,如果小于500毫秒,认为是非人操作^_^,我们让它失败重来.
4.最后将验证结果存入redis中。删除本地的验证码缓存信息,防止二次使用。
5.登陆的时候,从redis中获取验证结果,如果结果成功,删除验证结果,进入登陆流程。

import cn.hutool.core.util.NumberUtil;import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;/*** 验证码** @author hexm* @date 2020/10/27 10:58*/
@RestController
@RequestMapping("/captcha")
public class CaptchaController {private static final int X_OFFSET = 8;private static final int SPEED = 500;@Autowiredprivate Cache<Object> cache;@ApiOperation("获取验证码")@GetMapping(value = "/")public CaptchaVo captcha() {// 删除上次验证结果cache.remove(CacheConstant.CAPTCHA_RESULT + ThreadContextHolder.getSession().getId());PuzzleCaptcha puzzleCaptcha = new PuzzleCaptcha(CaptchaUtil.randomImage());puzzleCaptcha.setImageQuality(Image.SCALE_AREA_AVERAGING);puzzleCaptcha.run();Map<String, Object> cacheMap = new HashMap<>();CaptchaVo captchaVo = new CaptchaVo();captchaVo.setImage1(ImageConvertUtil.toDataUri(puzzleCaptcha.getArtwork(), "png"));captchaVo.setImage2(ImageConvertUtil.toDataUri(puzzleCaptcha.getVacancy(), "png"));// 偏移量cacheMap.put("x", puzzleCaptcha.getX());cacheMap.put("time", System.currentTimeMillis());cacheMap.put("width", puzzleCaptcha.getWidth());cache.put(CacheConstant.CAPTCHA + ThreadContextHolder.getSession().getId(), cacheMap, 5 * 60);return captchaVo;}@ApiOperation("验证码验证")@PostMapping(value = "/verify")public CaptchaResult verify(@RequestBody Map<String, Object> map) {CaptchaResult result = new CaptchaResult();result.setSuccess(false);String key = CacheConstant.CAPTCHA + ThreadContextHolder.getSession().getId();// 偏移量Integer vx = StrUtil.toInt(map.get("x"));// 宽度Integer width = StrUtil.toInt(map.get("width"), 1);//缓存Map<String, Object> cacheMap = cache.get(key);if (cacheMap == null) {result.setMessage(ServiceErrorCode.E1008.desc());return result;}Integer x = StrUtil.toInt(cacheMap.get("x"));Integer realWidth = StrUtil.toInt(cacheMap.get("width"));Long time = StrUtil.toLong(cacheMap.get("time"));// 验证速度long s = System.currentTimeMillis() - time;// 查看前端的缩放比例double ratio = NumberUtil.div(realWidth, width).doubleValue();if (x == null || vx == null) {result.setMessage(ServiceErrorCode.E1008.desc());cache.remove(key);return result;} else if (Math.abs(x - (vx * ratio)) > X_OFFSET * ratio || s < SPEED) {result.setMessage(ServiceErrorCode.E1009.desc());cache.remove(key);return result;}result.setSuccess(true);cache.remove(key);cache.put(CacheConstant.CAPTCHA_RESULT + ThreadContextHolder.getSession().getId(), result, 5 * 60);return result;}
}

图片转base64 ImageConvertUtil

这里没有使用hutool的ImgUtil.toBase64是因为该工具类png格式的图片透明背景会变成黑色。希望更新版本后会修复这个bug。

import com.jhlabs.image.ImageUtils;import javax.imageio.ImageIO;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;/*** @author hexm* @date 2020/10/27 15:37*/
public class ImageConvertUtil {/*** 将image对象转为base64字符串** @param image* @return*/public static String toBase64(Image image, String format) {return Base64.getEncoder().encodeToString(toBytes(image, format));}/*** 将image对象转为前端img标签识别的base64字符串** @param image* @param format* @return*/public static String toDataUri(Image image, String format) {return String.format("data:image/%s;base64,%s", format, toBase64(image, format));}/*** 将image对象转为字节** @param image* @param format* @return*/public static byte[] toBytes(Image image, String format) {ByteArrayOutputStream stream = new ByteArrayOutputStream();try {ImageIO.write(ImageUtils.convertImageToARGB(image), format, stream);} catch (IOException e) {e.printStackTrace();}return stream.toByteArray();}
}

前端vue

修改的地方不会很多,大部分删减后移动到了后端

<template><!-- 本体部分 --><div:id="id":class="['vue-puzzle-vcode', { show_: show }]"@mousedown="onCloseMouseDown"@mouseup="onCloseMouseUp"@touchstart="onCloseMouseDown"@touchend="onCloseMouseUp"><div class="vue-auth-box_" :style="{'border-radius':borderRadius+'px'}" @mousedown.stop @touchstart.stop><div class="auth-body_" :style="`height: ${canvasHeight}px`"><!-- 主图,有缺口 --><img ref="img1" :src="data:image1" alt="" :width="canvasWidth" :height="canvasHeight"><!-- 小图 --><imgref="img2":src="data:image2"class="auth-canvas2_":height="canvasHeight":style="`transform:translateX(${styleWidth - sliderBaseSize}px)`"alt=""><div :class="['loading-box_', { hide_: !loading }]"><div class="loading-gif_"><span /><span /><span /><span /><span /></div></div><div :class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]">{{ infoText }}</div><div:class="['flash_', { show: isSuccess }]":style="`transform: translateX(${isSuccess? `${canvasWidth + canvasHeight * 0.578}px`: `-${canvasHeight * 0.578}px`}) skew(-30deg, 0);`"/><img class="reset_" :src="resetSvg" @click="reset"></div><div class="auth-control_"><div class="range-box" :style="`height:${sliderBaseSize}px`"><div class="range-text">{{ sliderText }}</div><div ref="range-slider" class="range-slider" :style="`width:${styleWidth}px`"><div :class="['range-btn', { isDown: mouseDown }]" :style="`width:${sliderBaseSize}px`" @mousedown="onRangeMouseDown($event)" @touchstart="onRangeMouseDown($event)"><div /><div /><div /></div></div></div></div></div></div>
</template><script>
import { captcha, captchaVerify } from '@/api/common/common'const resetSvg = require('@/components/PuzzleCode/reset.png')
export default {name: 'PuzzleCode',/** 父级参数 **/props: {id: { type: String, default: undefined },canvasWidth: { type: Number, default: 280 }, // 主canvas的宽canvasHeight: { type: Number, default: 150 }, // 主canvas的高// 是否出现,由父级控制show: { type: Boolean, default: false },sliderSize: { type: Number, default: 35 }, // 滑块的大小successText: {type: String,default: '验证通过!'},failText: {type: String,default: '验证失败,请重试'},sliderText: {type: String,default: '拖动滑块完成拼图'},borderRadius: {type: Number,default: 10}},/** 私有数据 **/data() {return {image1: undefined, // 大图image2: undefined, // 小图mouseDown: false, // 鼠标是否在按钮上按下startWidth: 50, // 鼠标点下去时父级的widthstartX: 0, // 鼠标按下时的XnewX: 0, // 鼠标当前的偏移Xloading: true, // 是否正在加载中,主要是等图片onloadisCanSlide: false, // 是否可以拉动滑动条error: false, // 图片加在失败会出现这个,提示用户手动刷新infoBoxShow: false, // 提示信息是否出现infoText: '', // 提示等信息infoBoxFail: false, // 是否验证失败timer1: null, // setTimout1closeDown: false, // 为了解决Mac上的click BUGisSuccess: false, // 验证成功resetSvg,isSubmit: false // 是否正在验证中}},/** 计算属性 **/computed: {// styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度styleWidth() {const w = this.startWidth + this.newX - this.startXreturn w < this.sliderBaseSize? this.sliderBaseSize: w > this.canvasWidth? this.canvasWidth: w},// 处理一下sliderSize,弄成整数,以免计算有偏差sliderBaseSize() {return Math.max(Math.min(Math.round(this.sliderSize),Math.round(this.canvasWidth * 0.5)),10)}},/** 监听 **/watch: {show(newV) {// 每次出现都应该重新初始化if (newV) {document.body.classList.add('vue-puzzle-overflow')this.reset()} else {document.body.classList.remove('vue-puzzle-overflow')}}},/** 生命周期 **/mounted() {document.body.appendChild(this.$el)document.addEventListener('mousemove', this.onRangeMouseMove, false)document.addEventListener('mouseup', this.onRangeMouseUp, false)document.addEventListener('touchmove', this.onRangeMouseMove, {passive: false})document.addEventListener('touchend', this.onRangeMouseUp, false)if (this.show) {document.body.classList.add('vue-puzzle-overflow')this.reset()}},beforeDestroy() {clearTimeout(this.timer1)document.body.removeChild(this.$el)document.removeEventListener('mousemove', this.onRangeMouseMove, false)document.removeEventListener('mouseup', this.onRangeMouseUp, false)document.removeEventListener('touchmove', this.onRangeMouseMove, {passive: false})document.removeEventListener('touchend', this.onRangeMouseUp, false)},/** 方法 **/methods: {// 关闭onClose() {if (!this.mouseDown) {clearTimeout(this.timer1)this.$emit('close')}},onCloseMouseDown() {this.closeDown = true},onCloseMouseUp() {if (this.closeDown) {this.onClose()}this.closeDown = false},// 鼠标按下准备拖动onRangeMouseDown(e) {if (this.isCanSlide) {this.mouseDown = truethis.startWidth = this.$refs['range-slider'].clientWidththis.newX = e.clientX || e.changedTouches[0].clientXthis.startX = e.clientX || e.changedTouches[0].clientX}},// 鼠标移动onRangeMouseMove(e) {if (this.mouseDown) {e.preventDefault()this.newX = e.clientX || e.changedTouches[0].clientX}},// 鼠标抬起onRangeMouseUp() {if (this.mouseDown) {this.mouseDown = falsethis.submit()}},/*** 开始进行*/init() {this.loading = truethis.isCanSlide = falsecaptcha().then(res => {this.image1 = res.image1this.image2 = res.image2this.loading = falsethis.isCanSlide = truethis.startTime = new Date().getTime()})},// 开始判定submit() {// 关闭拖动this.isCanSlide = falsethis.isSubmit = trueconst x = this.newX - this.startXthis.loading = truecaptchaVerify({ x, width: this.canvasWidth }).then(res => {this.isSubmit = falsethis.loading = falseif (res.success) {// 成功this.infoText = this.successTextthis.infoBoxFail = falsethis.infoBoxShow = truethis.isCanSlide = falsethis.isSuccess = true// 成功后准备关闭clearTimeout(this.timer1)this.timer1 = setTimeout(() => {// 成功的回调this.$emit('success', x)}, 800)} else {// 失败this.infoText = this.failTextthis.infoBoxFail = truethis.infoBoxShow = truethis.isCanSlide = false// 失败的回调this.$emit('fail', x)// 800ms后重置clearTimeout(this.timer1)this.timer1 = setTimeout(() => {this.reset()}, 800)}})},// 重置reset() {this.infoBoxFail = falsethis.infoBoxShow = falsethis.isCanSlide = truethis.isSuccess = falsethis.startWidth = this.sliderBaseSize // 鼠标点下去时父级的widththis.startX = 0 // 鼠标按下时的Xthis.newX = 0 // 鼠标当前的偏移Xthis.init()}}
}
</script>
<style lang="scss">
.vue-puzzle-vcode {position: fixed;top: 0;left: 0;bottom: 0;right: 0;background-color: rgba(0, 0, 0, 0.3);z-index: 999;opacity: 0;pointer-events: none;transition: opacity 200ms;&.show_ {opacity: 1;pointer-events: auto;}
}
.vue-auth-box_ {position: absolute;top: 40%;left: 50%;transform: translate(-50%, -50%);padding: 20px;background: #fff;user-select: none;border-radius: 3px;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);.auth-body_ {position: relative;overflow: hidden;border-radius: 3px;.loading-box_ {position: absolute;top: 0;left: 0;bottom: 0;right: 0;background-color: rgba(0, 0, 0, 0.8);z-index: 20;opacity: 1;transition: opacity 200ms;display: flex;align-items: center;justify-content: center;&.hide_ {opacity: 0;pointer-events: none;.loading-gif_ {span {animation-play-state: paused;}}}.loading-gif_ {flex: none;height: 5px;line-height: 0;@keyframes load {0% {opacity: 1;transform: scale(1.3);}100% {opacity: 0.2;transform: scale(0.3);}}span {display: inline-block;width: 5px;height: 100%;margin-left: 2px;border-radius: 50%;background-color: #888;animation: load 1.04s ease infinite;&:nth-child(1) {margin-left: 0;}&:nth-child(2) {animation-delay: 0.13s;}&:nth-child(3) {animation-delay: 0.26s;}&:nth-child(4) {animation-delay: 0.39s;}&:nth-child(5) {animation-delay: 0.52s;}}}}.info-box_ {position: absolute;bottom: 0;left: 0;width: 100%;height: 24px;line-height: 24px;text-align: center;overflow: hidden;font-size: 13px;background-color: #83ce3f;opacity: 0;transform: translateY(24px);transition: all 200ms;color: #fff;z-index: 10;&.show {opacity: 0.95;transform: translateY(0);}&.fail {background-color: #ce594b;}}.auth-canvas2_ {position: absolute;top: 0;left: 0;z-index: 2;}.auth-canvas3_ {position: absolute;top: 0;left: 0;opacity: 0;transition: opacity 600ms;z-index: 3;&.show {opacity: 1;}}.flash_ {position: absolute;top: 0;left: 0;width: 30px;height: 100%;background-color: rgba(255, 255, 255, 0.1);z-index: 3;&.show {transition: transform 600ms;}}.reset_ {position: absolute;top: 2px;right: 2px;width: 35px;height: auto;z-index: 12;cursor: pointer;transition: transform 200ms;transform: rotate(0deg);&:hover {transform: rotate(-90deg);}}}.auth-control_ {.range-box {position: relative;width: 100%;background-color: #eef1f8;margin-top: 20px;border-radius: 3px;box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset;.range-text {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 14px;color: #b7bcd1;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;text-align: center;width: 100%;}.range-slider {position: absolute;height: 100%;width: 50px;background-color: rgba(106, 160, 255, 0.8);border-radius: 3px;.range-btn {position: absolute;display: flex;align-items: center;justify-content: center;right: 0;width: 50px;height: 100%;background-color: #fff;border-radius: 3px;box-shadow: 0 0 4px #ccc;cursor: pointer;& > div {width: 0;height: 40%;transition: all 200ms;&:nth-child(2) {margin: 0 4px;}border: solid 1px #6aa0ff;}&:hover,&.isDown {& > div:first-child {border: solid 4px transparent;height: 0;border-right-color: #6aa0ff;}& > div:nth-child(2) {border-width: 3px;height: 0;border-radius: 3px;margin: 0 6px;border-right-color: #6aa0ff;}& > div:nth-child(3) {border: solid 4px transparent;height: 0;border-left-color: #6aa0ff;}}}}}}
}
.vue-puzzle-overflow {overflow: hidden !important;
}
</style>

api

/*** 验证码* @returns {*}*/
export function captcha() {return request({url: `/captcha/`,method: 'get'})
}/*** 验证码验证* @returns {*}*/
export function captchaVerify(data) {return request({url: `/captcha/verify`,method: 'post',headers: { 'Content-Type': 'application/json' },data})
}

好了,代码基本已经结束,reset.png图片找不到的,可以到vue-puzzle-vcode里面下载,或者替换成其他图片也是可以的。

最后成品

扩展GIf动态拼图

既然已经实现了png的拼图,我们来看一下gif动态拼图的可能性。
首先,gif格式不支持半透明的效果,也就是说小图的外阴影不能用了,会有白色的边,我们改成内阴影

看下效果,其实还行,就是处理速度有点慢。

总体的差别不多,使用多线程加速一下,否则多帧图像处理速度会比较慢。
下面是实现的代码,使用上没有太大的差别

import cn.hutool.core.img.gif.AnimatedGifEncoder;
import cn.hutool.core.img.gif.GifDecoder;
import cn.hutool.core.util.NumberUtil;
import com.jhlabs.image.ImageUtils;
import com.jhlabs.image.InvertAlphaFilter;
import com.jhlabs.image.ShadowFilter;
import lombok.Data;import java.awt.*;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;/*** gif滑块验证码** @author hexm* @date 2020/10/23*/
@Data
public class PuzzleGifCaptcha {/** 默认宽度,用来计算阴影基本长度 */private static final int DEFAULT_WIDTH = 280;/** 随机数 */private static final Random RANDOM = new Random();/** 蒙版 */private static Color color = new Color(255, 255, 255, 204);/** alpha通道过滤器 */private static InvertAlphaFilter alphaFilter = new InvertAlphaFilter();/** 边距 */private static int margin = 10;/** 生成图片的宽度 */private int width = DEFAULT_WIDTH;/** 生成图片高度 */private int height = 150;/** x轴的坐标,由算法决定 */private int x;/** y轴的坐标,由算法决定 */private int y;/** 拼图长宽 */private int vwh = 10 * 3;/** 原图 */private InputStream image;/** 大图 */private ByteArrayOutputStream artwork;/** 小图 */private ByteArrayOutputStream vacancy;/** 是否注重速度 */private boolean isFast = false;/** 小图描边颜色 */private Color vacancyBorderColor;/** 小图描边线条的宽度 */private float vacancyBorderWidth = 2.5f;/** 主图描边的颜色 */private Color artworkBorderColor;/** 主图描边线条的宽度 */private float artworkBorderWidth = 5f;/*** 最高放大倍数,合理的放大倍数可以使图像平滑且提高渲染速度* 当isFast为false时,此属性生效* 放大倍数越高,生成的图像越平滑,受原始图片大小的影响。*/private double maxRatio = 2;/*** 画质** @see Image#SCALE_DEFAULT* @see Image#SCALE_FAST* @see Image#SCALE_SMOOTH* @see Image#SCALE_REPLICATE* @see Image#SCALE_AREA_AVERAGING*/private int imageQuality = Image.SCALE_SMOOTH;/** 开启多线程处理 */private boolean multithreading = true;/*** 从流中读取图片** @param is*/public PuzzleGifCaptcha(InputStream is) {image = is;}/*** 从文件中读取图片** @param fileName*/public PuzzleGifCaptcha(String fileName) throws FileNotFoundException {image = new FileInputStream(fileName);}/*** 生成随机x、y坐标*/private void init() {if (x == 0 || y == 0) {this.x = random(vwh, this.width - vwh - margin);this.y = random(margin, this.height - vwh - margin);}}/*** 执行*/public void run() throws IOException {init();GifDecoder decoder = new GifDecoder();int status = decoder.read(image);if (status != GifDecoder.STATUS_OK) {throw new IOException("read image error!");}AnimatedGifEncoder e = new AnimatedGifEncoder();AnimatedGifEncoder e2 = new AnimatedGifEncoder();ByteArrayOutputStream b1 = new ByteArrayOutputStream();ByteArrayOutputStream b2 = new ByteArrayOutputStream();//保存的目标图片e.start(b1);e2.start(b2);e.setRepeat(decoder.getLoopCount());e2.setRepeat(decoder.getLoopCount());e.setDelay(decoder.getDelay(0));e2.setDelay(decoder.getDelay(0));e2.setTransparent(Color.white);if (multithreading) {// 多线程CompletableFuture<BufferedImage[]>[] futures = new CompletableFuture[decoder.getFrameCount()];for (int i = 0; i < decoder.getFrameCount(); i++) {int finalI = i;futures[i] = CompletableFuture.supplyAsync(() -> {BufferedImage image = decoder.getFrame(finalI);//可以加入对图片的处理,比如缩放,压缩质量return process(image);});}CompletableFuture.allOf(futures).join();for (CompletableFuture<BufferedImage[]> future : futures) {try {BufferedImage[] bufferedImages = future.get();e.addFrame(bufferedImages[0]);e2.addFrame(bufferedImages[1]);} catch (InterruptedException | ExecutionException interruptedException) {interruptedException.printStackTrace();}}} else {// 单线程for (int i = 0; i < decoder.getFrameCount(); i++) {BufferedImage image = decoder.getFrame(i);//可以加入对图片的处理,比如缩放,压缩质量BufferedImage[] bufferedImages = process(image);e.addFrame(bufferedImages[0]);e2.addFrame(bufferedImages[1]);}}e.finish();e2.finish();this.artwork = b1;this.vacancy = b2;if (image != null) {image.close();}}private BufferedImage[] process(Image image) {// 缩略图Image thumbnail;GeneralPath path;int realW = image.getWidth(null);int realH = image.getHeight(null);int w = realW, h = realH;double wScale = 1, hScale = 1;// 如果原始图片比执行的图片还小,则先拉伸再裁剪boolean isFast = this.isFast || w < this.width || h < this.height;if (isFast) {// 缩放,使用平滑模式thumbnail = image.getScaledInstance(width, height, imageQuality);path = paintBrick(1, 1);w = this.width;h = this.height;} else {// 缩小到一定的宽高,保证裁剪的圆润boolean flag = false;if (realW > width * maxRatio) {// 不超过最大倍数且不超过原始图片的宽w = Math.min((int) (width * maxRatio), realW);flag = true;}if (realH > height * maxRatio) {h = Math.min((int) (height * maxRatio), realH);flag = true;}if (flag) {// 若放大倍数生效,则缩小图片至最高放大倍数,再进行裁剪thumbnail = image.getScaledInstance(w, h, imageQuality);} else {thumbnail = image;}hScale = NumberUtil.div(h, height);wScale = NumberUtil.div(w, width);path = paintBrick(wScale, hScale);}// 创建阴影过滤器float radius = 5 * ((float) w / DEFAULT_WIDTH) * (float) wScale;ShadowFilter shadowFilter = new ShadowFilter(radius, 2 * (float) wScale, -1 * (float) hScale, 0.8f);// 创建空白的图片BufferedImage artwork = translucent(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB));BufferedImage localVacancy = translucent(new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB));// 画小图Graphics2D vg = localVacancy.createGraphics();// 抗锯齿vg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 设置画图路径范围vg.setClip(path);// 将区域中的图像画到小图中vg.drawImage(thumbnail, null, null);//描边if (vacancyBorderColor != null) {vg.setColor(vacancyBorderColor);vg.setStroke(new BasicStroke(vacancyBorderWidth));vg.draw(path);}// 画上基于小图的内阴影,先反转alpha通道,然后创建阴影BufferedImage shadowImage = shadowFilter.filter(alphaFilter.filter(localVacancy, null), null);// 画上内阴影小图vg.drawImage(shadowImage, null, null);// 释放图像vg.dispose();// 画大图// 创建画笔Graphics2D g = artwork.createGraphics();// 抗锯齿g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 画上图片g.drawImage(thumbnail, null, null);// 设置画图路径范围g.setClip(path);// 填充缺口透明度 颜色混合,不透明在上g.setComposite(AlphaComposite.SrcAtop);// 填充一层白色的透明蒙版,透明度越高,白色越深 alpha:0-255g.setColor(color);g.fill(path);//描边if (artworkBorderColor != null) {g.setColor(artworkBorderColor);g.setStroke(new BasicStroke(artworkBorderWidth));g.draw(path);}// 画上内阴影小图g.drawImage(shadowImage, null, null);// 释放图像g.dispose();// 裁剪掉多余的透明背景localVacancy = ImageUtils.getSubimage(localVacancy, (int) (x * wScale), 0, (int) Math.ceil(path.getBounds().getWidth()), h);BufferedImage[] bufferedImages = new BufferedImage[2];if (isFast) {// 添加阴影bufferedImages[0] = localVacancy;bufferedImages[1] = artwork;} else {// 大图缩放bufferedImages[0] = ImageUtils.convertImageToARGB(artwork.getScaledInstance(width, height, imageQuality));// 缩放时,除以放大比例bufferedImages[1] = ImageUtils.convertImageToARGB(localVacancy.getScaledInstance((int) (path.getBounds().getWidth() / wScale), height, imageQuality));}return bufferedImages;}/*** 绘制拼图块的路径** @param xScale x轴放大比例* @param yScale y轴放大比例* @return*/private GeneralPath paintBrick(double xScale, double yScale) {double x = this.x * xScale;double y = this.y * yScale;// 直线移动的基础距离double hMoveL = vwh / 3f * yScale;double wMoveL = vwh / 3f * xScale;GeneralPath path = new GeneralPath();path.moveTo(x, y);path.lineTo(x + wMoveL, y);// 上面的圆弧正东方向0°,顺时针负数,逆时针正数path.append(arc(x + wMoveL, y - hMoveL / 2, wMoveL, hMoveL, 180, -180), true);path.lineTo(x + wMoveL * 3, y);path.lineTo(x + wMoveL * 3, y + hMoveL);// 右边的圆弧path.append(arc(x + wMoveL * 2 + wMoveL / 2, y + hMoveL, wMoveL, hMoveL, 90, -180), true);path.lineTo(x + wMoveL * 3, y + hMoveL * 3);path.lineTo(x, y + hMoveL * 3);path.lineTo(x, y + hMoveL * 2);// 左边的内圆弧path.append(arc(x - wMoveL / 2, y + hMoveL, wMoveL, hMoveL, -90, 180), true);path.lineTo(x, y);path.closePath();return path;}/*** 绘制圆形、圆弧或者是椭圆形* 正东方向0°,顺时针负数,逆时针正数** @param x      左上角的x坐标* @param y      左上角的y坐标* @param w      宽* @param h      高* @param start  开始的角度* @param extent 结束的角度* @return*/private Arc2D arc(double x, double y, double w, double h, double start, double extent) {return new Arc2D.Double(x, y, w, h, start, extent, Arc2D.OPEN);}/*** 透明背景** @param bufferedImage* @return*/private BufferedImage translucent(BufferedImage bufferedImage) {Graphics2D g = bufferedImage.createGraphics();bufferedImage = g.getDeviceConfiguration().createCompatibleImage(bufferedImage.getWidth(), bufferedImage.getHeight(), Transparency.TRANSLUCENT);g.dispose();return bufferedImage;}/*** 随机数** @param min* @param max* @return*/private static int random(int min, int max) {return RANDOM.ints(min, max + 1).findFirst().getAsInt();}
}

引用

  1. 纯前端验证码:https://github.com/javaLuo/vue-puzzle-vcode
  2. 源码示例:https://github.com/yixiaco/puzzle_captcha

java和vue实现滑动拼图验证码相关推荐

  1. uniapp、vue实现滑动拼图验证码

    实际开发工作中,在登陆的时候需要短信验证码,但容易引起爬虫行为,需要用到反爬虫验证码,今天介绍一下拼图验证码,解决验证码反爬虫中的滑动验证码反爬虫. 原理 滑动拼图验证码是在滑块验证码的基础上增加了一 ...

  2. 滑动拼图验证码操作步骤:_拼图项目:一个不完整的难题

    滑动拼图验证码操作步骤: 马克·雷因霍尔德(Mark Reinhold)最近提议延迟Java 9,以花更多的时间完成项目Jigsaw,这是即将发布的版本的主要功能. 虽然这个决定肯定会使Java的厄运 ...

  3. 滑动拼图验证码操作步骤:_拼图项目:延期的后果

    滑动拼图验证码操作步骤: Mark Reinhold先生于2012年7月宣布 ,他们计划从Java 8撤消Jigsaw项目 ,因为Jigsaw计划于2013年9月(从现在开始一年)推迟其发布. 这个日 ...

  4. html 滑动拼图验证,vue登录滑动拼图验证

    vue登录滑动拼图验证 vue登录滑动拼图验证 一.安装插件: npm install --save vue-monoplasty-slide-verify 二.main.js引入: import S ...

  5. Android 滑动拼图验证码控件

    Android 滑动拼图验证码控件 简介: 很多软件为了安全防止恶意攻击,会在登录/注册时进行人机验证,常见的人机验证方式有:谷歌点击复选框进行验证,输入验证码验证,短信验证码,语音验证,文字按顺序选 ...

  6. 小视频app源码,Android 滑动拼图验证码控件

    小视频app源码,Android 滑动拼图验证码控件 代码实现: 滑块视图类:SlideImageView.java.实现小视频APP源码随机选取拼图位置,对拼图位置进行验证等功能. public c ...

  7. 爬虫之极验验证码破解-滑动拼图验证码破解

    滑动拼图验证码破解 前言 步骤分析 第一步,获取原图 第二步 拼接图片 第三步 计算豁口所在位置 第四步 计算拖动距离 模拟拖动 其他 前言 滑动验证码已经流行很多年了,我们在这里尝试一下如何实现滑动 ...

  8. js php滑动拼图解锁,js 滑动拼图验证码

    以前的验证码很简单,就是一个带些背景色或背景图和干扰线的纯数字字母类的验证码,现在已经发展变得很丰富了.我见过的就有好几种:纯字母数字类,数学计算类,依次点击图片上的文字类,从下列图片列表里选取符合描 ...

  9. 滑动拼图验证码 免费 java_js+canvas实现滑动拼图验证码功能

    上图为网易云盾的滑动拼图验证码,其应该有一个专门的图片库,裁剪的位置是固定的.我的想法是,随机生成图片,随机生成位置,再用canvas裁剪出滑块和背景图.下面介绍具体步骤. 首先随便找一张图片渲染到c ...

  10. php滑动拼图验证,JS怎么实现滑动拼图验证码

    这次给大家带来JS怎么实现滑动拼图验证码,JS实现滑动拼图验证码的注意事项有哪些,下面就是实战案例,一起来看一下. 上图为网易云盾的滑动拼图验证码,其应该有一个专门的图片库,裁剪的位置是固定的.我的想 ...

最新文章

  1. Nature综述:从土壤到临床-微生物次级代谢产物对抗生素耐受性和耐药性的影响...
  2. FPGA设计思想之“逻辑复制”
  3. 惊天大谎:让穷人都能上网是Facebook的殖民阴谋?
  4. android.content.Context.getResources()‘ on a null object reference
  5. Linux文件atime ctime mtime
  6. C#操作Excel的OLEDB方式与COM方式比较
  7. oxyen eclipse 启动 报错 se启动提示javaw.exe in your current PATH、No java virtual machine
  8. IntelliJ IDEA 中的Java Web项目的资源文件复制新增如何更新到部署包中?
  9. POI3.8解决导出大数据量excel文件时内存溢出的问题
  10. Git(8)-- 撤消操作(git commit --amend、git reset 和 git checkout 命令详解)
  11. pycharm: connot find declaration to go to
  12. docker安装jdk8
  13. Voltage Keepsake CodeForces - 801C(二分)
  14. c语言结构体的实例使用
  15. i3wm nm-applet每次开机都要输入wifi密码的解决办法
  16. html表单代码有哪些,HTML常用代码有哪些
  17. JMETER badboy 下载及安装
  18. java对接PayPal支付 (添加物流跟踪信息)
  19. Mixpanel接入
  20. 单通道图片转换为3通道图片,实现灰度图上添加彩色标注

热门文章

  1. “阿里云OS”是如何失控的
  2. TikZ绘图示例——尺规作图:过直线外一点作给定直线的平行线
  3. 教你如何查询车辆出险记录
  4. Github Actions生成 secrets
  5. 关闭IDEA提示 empty tag doesn't work in some browsers(设置inspections)
  6. deepin访问不了网页
  7. 数据分析/机器学习 350+ 数据集链接整理,免费下载点开就用
  8. 全球与中国太阳能并网逆变器市场深度研究分析报告
  9. 人生有三重境界:看山是山,看水是水;看山不是山,看水不是水;看山还是山,看水还是水(转载)
  10. C语言求N阶乘的方法