vue 滑动拼图验证 + 后端验证完整流程
百度了许久都没找到一个像样的demo只好自己改写了我是后端开发前端css样式不怎么擅长所以参考了https://www.sucaihuo.com/js/3718这个demo有兴趣的可以看看前端模块这里我大概说一下LoginSlider 这个就是我的滑块登录验证的组件:randomStr :随机码 这个随机码就相当于session 唯一标识因为我这里没有采用...
百度了许久都没找到一个像样的demo 只好自己改写了 我是后端开发 前端css样式不怎么擅长 所以参考了 https://www.sucaihuo.com/js/3718这个demo有兴趣的可以看看
前端模块
这里我大概说一下
LoginSlider 这个就是我的滑块登录验证的组件
:randomStr :随机码 这个随机码就相当于session 唯一标识 因为我这里没有采用session的方式 所以传了个随机码
@parentHandleSubmit:vue子父传值与调用应该没啥好说的了吧
<LoginSlider ref="loginSliderRef" :randomStr="form.randomStr" @parentHandleSubmit="handleSubmit"></LoginSlider>
子组件通过emit(“parentHandleSubmit”,code)的方式请求了登录方法handleSubmit
其他地方都是我的验证逻辑和登录接口的请求 主要看调用
拖动验证登录成功 this.refs.loginSliderRef.onSuccess() 美元符合打不出来 自己看下面的代码
拖动验证登录失败 this.refs.loginSliderRef.onFail() 美元符合打不出来 自己看下面的代码
handleSubmit (code) {
this.form.code = code
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.$store.dispatch('LoginByUsername', this.form).then(() => {
/** 拖动验证登录成功 **/
this.$refs.loginSliderRef.onSuccess()
this.$router.push({ name: this.$config.homeName })
}).catch(error => {
/** 拖动验证登录失败 **/
this.$refs.loginSliderRef.onFail()
})
}
})
}
子组件—滑块模块
这里的基本上封装好的了 主要要改的是获得图片的接口
this.initCodeImg() /** 初始化拼图验证码 **/
<template>
<div class="captcha" ref="captcha" style="position: relative"></div>
</template>
<script>
import '_v/admin/login/slider/jigsaw.css'
import { getCode } from '_a/admin/login'
export default {
name: 'LoginSlider',
props:{
randomStr:{ type: String}
},
data() {
return {
formCustom: {
w: 288,
h: 155,
bigImage: '',
smallImage: '',
yHeight: 0,
trail: []
},
DOM:{
bigImage: '',
block: '',
sliderContainer: '',
sliderPanel: '',
refreshIcon: '',
sliderMask: '',
slider: '',
sliderIcon: '',
text: ''
}
}
},
mounted() {
this.initDOM(this.$refs.captcha)
this.bindEvents(this.$refs.captcha)
this.initCodeImg()
},
methods: {
initCodeImg(){
/** 初始化拼图验证码 **/
return new Promise((resolve, reject) => {
getCode({randomStr:this.randomStr}).then(res => {
if(res.code === 200){
this.formCustom = res.data
this.initImg()
}
resolve()
}).catch(error => {
reject(error)
})
})
},
onVerify(code){
/** 传给父组件 请求登录验证 **/
this.$emit('parentHandleSubmit',code)
},
onSuccess(){
this.addClass(this.DOM.sliderContainer, 'sliderContainer_success')
},
onFail(){
this.addClass(this.DOM.sliderContainer, 'sliderContainer_fail')
this.reset()
},
initDOM(el) {
this.DOM.bigImage = this.createElement('img') // 大图
this.DOM.block = this.createElement('img') // 滑块
this.DOM.sliderContainer = this.createElement('div')
this.DOM.sliderPanel = this.createElement('div')
this.DOM.refreshIcon = this.createElement('div')
this.DOM.sliderMask = this.createElement('div')
this.DOM.slider = this.createElement('div')
this.DOM.sliderIcon = this.createElement('span')
this.DOM.text = this.createElement('span')
this.DOM.sliderPanel.className = 'sliderPanel'
this.DOM.block.className = 'block'
this.DOM.sliderContainer.className = 'sliderContainer'
this.DOM.refreshIcon.className = 'refreshIcon'
this.DOM.sliderMask.className = 'sliderMask'
this.DOM.slider.className = 'slider'
this.DOM.sliderIcon.className = 'sliderIcon'
this.DOM.text.innerHTML = '向右滑动滑块填充拼图'
this.DOM.text.className = 'sliderText'
el.appendChild(this.DOM.sliderPanel)
this.DOM.sliderPanel.appendChild(this.DOM.bigImage)
this.DOM.sliderPanel.appendChild(this.DOM.refreshIcon)
this.DOM.sliderPanel.appendChild(this.DOM.block)
this.DOM.slider.appendChild(this.DOM.sliderIcon)
this.DOM.sliderMask.appendChild(this.DOM.slider)
this.DOM.sliderContainer.appendChild(this.DOM.sliderMask)
this.DOM.sliderContainer.appendChild(this.DOM.text)
el.appendChild(this.DOM.sliderContainer)
},
initImg() {
this.DOM.bigImage.src = this.formCustom.bigImage
this.DOM.block.src = this.formCustom.smallImage
this.DOM.block.style = 'padding-top:'+this.formCustom.yHeight+'px'
},
createElement(tagName) {
return document.createElement(tagName)
},
bindEvents(el){
let that = this
el.onselectstart = () => false
that.DOM.refreshIcon.onclick = () => {
that.initCodeImg()
}
let originX, originY,blockLeft, trail = [], isMouseDown = false
/** 鼠标点击 **/
that.DOM.slider.addEventListener('mousedown', function (e) {
originX = e.x, originY = e.y
isMouseDown = true
})
const formCustomW = that.formCustom.w
/** 鼠标拖动 **/
document.addEventListener('mousemove', (e) => {
if (!isMouseDown) return false
const moveX = e.x - originX
const moveY = e.y - originY
if (moveX < 0 || moveX + 38 >= formCustomW) return false
that.DOM.slider.style.left = moveX + 'px'
blockLeft = (formCustomW - 40 - 20) / (formCustomW - 40) * moveX
that.DOM.block.style.left = blockLeft + 'px'
that.addClass(that.DOM.sliderContainer, 'sliderContainer_active')
that.DOM.sliderMask.style.width = moveX + 'px'
trail.push(moveY)
})
/** 鼠标放开 **/
document.addEventListener('mouseup', (e) => {
if (!isMouseDown) return false
isMouseDown = false
if (e.x == originX) return false
that.removeClass(that.DOM.sliderContainer, 'sliderContainer_active')
that.trail = trail
that.onVerify(parseInt(blockLeft))
})
},
addClass(tag, className) {
tag.classList.add(className)
},
removeClass(tag, className) {
tag.classList.remove(className)
},
reset() {
this.DOM.sliderContainer.className = 'sliderContainer'
this.DOM.slider.style.left = 0
this.DOM.block.style.left = 0
this.DOM.sliderMask.style.width = 0
this.initCodeImg()
}
}
};
</script>
引入css样式
.block {
position: absolute;
left: 0;
top: 0;
}
.sliderContainer {
position: relative;
text-align: center;
width: 288px;
height: 40px;
line-height: 40px;
background: #f7f9fa;
color: #45494c;
border: 1px solid #e4e7eb;
}
.sliderContainer_active .slider {
height: 38px;
top: -1px;
border: 1px solid #1991FA;
}
.sliderContainer_active .sliderMask {
height: 38px;
border-width: 1px;
}
.sliderContainer_success .slider {
height: 38px;
top: -1px;
border: 1px solid #52CCBA;
background-color: #52CCBA !important;
}
.sliderContainer_success .sliderMask {
height: 38px;
border: 1px solid #52CCBA;
background-color: #D2F4EF;
}
.sliderContainer_success .sliderIcon {
background-position: 0 0 !important;
}
.sliderContainer_fail .slider {
height: 38px;
top: -1px;
border: 1px solid #f57a7a;
background-color: #f57a7a !important;
}
.sliderContainer_fail .sliderMask {
height: 38px;
border: 1px solid #f57a7a;
background-color: #fce1e1;
}
.sliderContainer_fail .sliderIcon {
background-position: 0 -83px !important;
}
.sliderContainer_active .sliderText, .sliderContainer_success .sliderText, .sliderContainer_fail .sliderText {
display: none;
}
.sliderMask {
position: absolute;
left: 0;
top: 0;
height: 40px;
border: 0 solid #1991FA;
background: #D1E9FE;
}
.slider {
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 40px;
background: #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: background .2s linear;
}
.slider:hover {
background: #1991FA;
}
.slider:hover .sliderIcon {
background-position: 0 -13px;
}
.sliderIcon {
position: absolute;
top: 15px;
left: 13px;
width: 14px;
height: 10px;
background: url(../../../../common/assets/images/login/icon_light.f13cff3.png) 0 -26px;
background-size: 34px 471px;
}
.refreshIcon {
position: absolute;
right: 0;
top: 0;
width: 34px;
height: 34px;
cursor: pointer;
background: url(../../../../common/assets/images/login/icon_light.f13cff3.png) 0 -437px;
background-size: 34px 471px;
}
.sliderPanel{
background-color: #ffffff;
/* Animation with transition in Safari and Chrome */
-webkit-transition: all 0.6s ease-in-out;
/* Animation with transition in Firefox (No supported Yet) */
-moz-transition: all 0.6s ease-in-out;
/* Animation with transition in Opera (No supported Yet)*/
-o-transition: all 0.6s ease-in-out;
/* The the opacity to 0 to create the fadeOut effect*/
opacity:0;
visibility:hidden;
position:absolute;
/* box shadow effect in Safari and Chrome*/
-webkit-box-shadow:#272229 2px 2px 10px;
/* box shadow effect in Firefox*/
-moz-box-shadow:#272229 2px 2px 10px;
/* box shadow effect in IE*/
filter:progid:DXImageTransform.Microsoft.Shadow(color='#272229', Direction=135, Strength=5);
/* box shadow effect in Browsers that support it, Opera 10.5 pre-alpha release*/
box-shadow:#272229 2px 2px 10px;
margin-top: 60px;
height: 150px;
}
.captcha:hover .sliderPanel{
opacity:1;
visibility:visible;
}
后端模块
import lombok.extern.slf4j.Slf4j;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.Random;
@Slf4j
public class ImageUtil {
static int targetWidth = 55;//小图长
static int targetHeight = 45;//小图宽
static int circleR = 8;//半径
static int r1 = 4;//距离点
/**
* @Description: 读取本地图片,生成拼图验证码
* @author zhoujin
* @return Map<String,Object> 返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标
*/
public static Map<String,Object> createImage(File file, Map<String,Object> resultMap){
try {
BufferedImage oriImage = ImageIO.read(file);
Random random = new Random();
//X轴距离右端targetWidth Y轴距离底部targetHeight以上
int widthRandom = random.nextInt(oriImage.getWidth()- 2*targetWidth) + targetWidth;
int heightRandom = random.nextInt(oriImage.getHeight()- targetHeight);
log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",oriImage.getWidth(),oriImage.getHeight(),widthRandom,heightRandom);
BufferedImage targetImage= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);
cutByTemplate(oriImage,targetImage,getBlockData(),widthRandom,heightRandom);
resultMap.put("bigImage", getImageBASE64(oriImage));//大图
resultMap.put("smallImage", getImageBASE64(targetImage));//小图
resultMap.put("xWidth",widthRandom);
resultMap.put("yHeight",heightRandom);
} catch (Exception e) {
log.info("创建图形验证码异常",e);
} finally{
return resultMap;
}
}
/**
* @Description: 读取网络图片,生成拼图验证码
* @author zhoujin
* @return Map<String,Object> 返回生成的抠图和带抠图阴影的大图 base64码及抠图坐标
*/
public static Map<String,Object> createImage(String imgUrl, Map<String,Object> resultMap){
try {
//通过URL 读取图片
URL url = new URL(imgUrl);
BufferedImage bufferedImage = ImageIO.read(url.openStream());
Random rand = new Random();
int widthRandom = rand.nextInt(bufferedImage.getWidth()- targetWidth - 100 + 1 ) + 100;
int heightRandom = rand.nextInt(bufferedImage.getHeight()- targetHeight + 1 );
log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})",bufferedImage.getWidth(),bufferedImage.getHeight(),widthRandom,heightRandom);
BufferedImage target= new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_4BYTE_ABGR);
cutByTemplate(bufferedImage,target,getBlockData(),widthRandom,heightRandom);
resultMap.put("bigImage", getImageBASE64(bufferedImage));//大图
resultMap.put("smallImage", getImageBASE64(target));//小图
resultMap.put("xWidth",widthRandom);
resultMap.put("yHeight",heightRandom);
} catch (Exception e) {
log.info("创建图形验证码异常",e);
} finally{
return resultMap;
}
}
/**
*
* @Createdate: 2019年1月24日上午10:51:30
* @Title: cutByTemplate
* @Description: 有这个轮廓后就可以依据这个二维数组的值来判定抠图并在原图上抠图位置处加阴影,
* @author zhoujin
* @param oriImage 原图
* @param targetImage 抠图拼图
* @param templateImage 颜色
* @param x
* @param y void
* @throws
*/
private static void cutByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] templateImage, int x, int y){
int[][] martrix = new int[3][3];
int[] values = new int[9];
//创建shape区域
for (int i = 0; i < targetWidth; i++) {
for (int j = 0; j < targetHeight; j++) {
int rgb = templateImage[i][j];
// 原图中对应位置变色处理
int rgb_ori = oriImage.getRGB(x + i, y + j);
if (rgb == 1) {
targetImage.setRGB(i, j, rgb_ori);
//抠图区域高斯模糊
readPixel(oriImage, x + i, y + j, values);
fillMatrix(martrix, values);
oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
}else{
//这里把背景设为透明
targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
}
}
}
}
private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
int xStart = x - 1;
int yStart = y - 1;
int current = 0;
for (int i = xStart; i < 3 + xStart; i++)
for (int j = yStart; j < 3 + yStart; j++) {
int tx = i;
if (tx < 0) {
tx = -tx;
} else if (tx >= img.getWidth()) {
tx = x;
}
int ty = j;
if (ty < 0) {
ty = -ty;
} else if (ty >= img.getHeight()) {
ty = y;
}
pixels[current++] = img.getRGB(tx, ty);
}
}
private static void fillMatrix(int[][] matrix, int[] values) {
int filled = 0;
for (int i = 0; i < matrix.length; i++) {
int[] x = matrix[i];
for (int j = 0; j < x.length; j++) {
x[j] = values[filled++];
}
}
}
private static int avgMatrix(int[][] matrix) {
int r = 0;
int g = 0;
int b = 0;
for (int i = 0; i < matrix.length; i++) {
int[] x = matrix[i];
for (int j = 0; j < x.length; j++) {
if (j == 1) {
continue;
}
Color c = new Color(x[j]);
r += c.getRed();
g += c.getGreen();
b += c.getBlue();
}
}
return new Color(r / 8, g / 8, b / 8).getRGB();
}
/**
* @Createdate: 2019年1月24日上午10:52:42
* @Title: getBlockData
* @Description: 生成小图轮廓
* @author zhoujin
* @return int[][]
* @throws
*/
private static int[][] getBlockData() {
int[][] data = new int[targetWidth][targetHeight];
double x2 = targetWidth -circleR; //47
//随机生成圆的位置
double h1 = circleR + Math.random() * (targetWidth-3*circleR-r1);
double po = Math.pow(circleR,2); //64
double xbegin = targetWidth - circleR - r1;
double ybegin = targetHeight- circleR - r1;
//圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
//计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
for (int i = 0; i < targetWidth; i++) {
for (int j = 0; j < targetHeight; j++) {
double d2 = Math.pow(j - 2,2) + Math.pow(i - h1,2);
double d3 = Math.pow(i - x2,2) + Math.pow(j - h1,2);
if ((j <= ybegin && d2 < po)||(i >= xbegin && d3 > po)) {
data[i][j] = 0;
} else {
data[i][j] = 1;
}
}
}
return data;
}
/**
* @Title: getImageBASE64
* @Description: 图片转BASE64
* @author zhoujin
* @param image
* @return
* @throws IOException String
*/
public static String getImageBASE64(BufferedImage image) throws IOException {
byte[] imagedata = null;
ByteArrayOutputStream bao=new ByteArrayOutputStream();
ImageIO.write(image,"png",bao);
imagedata=bao.toByteArray();
BASE64Encoder encoder = new BASE64Encoder();
String BASE64IMAGE=encoder.encodeBuffer(imagedata).trim();
BASE64IMAGE = BASE64IMAGE.replaceAll("\r|\n", ""); //删除 \r\n
return "data:image/png;base64," + BASE64IMAGE;
}
}
这一块可以自己去修改自己的随机图片目录 我这边的demo就先写死了
ImageUtil.createImage(“https://picsum.photos/id/692/300/150”,resultMap);
DEFAULT_CODE_KEY_随机值 保存到服务器redis缓存 1分钟
Map<String, Object> resultMap = new HashMap<>();
ImageUtil.createImage("https://picsum.photos/id/692/300/150",resultMap);
int xWidth = Integer.valueOf(resultMap.get("xWidth").toString());
log.info("验证值"+xWidth);
resultMap.remove("xWidth");
//保存验证码信息
String randomStr = serverRequest.queryParam("randomStr").get();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.opsForValue().set(CommonConstants.DEFAULT_CODE_KEY + randomStr, xWidth , SecurityConstants.CODE_TIME, TimeUnit.SECONDS);
验证code 我取的是一个15差值的范围 如果在这个范围内 我也算他成功
/**
* 检查code
*
* @param request
*/
@SneakyThrows
private void checkCode(ServerHttpRequest request) {
String code = request.getQueryParams().getFirst("code");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
String randomStr = request.getQueryParams().getFirst("randomStr");
String key = CommonConstants.DEFAULT_CODE_KEY + randomStr;
redisTemplate.setKeySerializer(new StringRedisSerializer());
if (!redisTemplate.hasKey(key)) {
throw new ApiException("验证码不合法");
}
Object codeObj = redisTemplate.opsForValue().get(key);
if (codeObj == null) {
throw new ApiException("验证码不合法");
}
String saveCode = codeObj.toString();
if (StrUtil.isBlank(saveCode)) {
redisTemplate.delete(key);
throw new ApiException("验证码不合法");
}
/** 判断验证值+15 是否在范围内 **/
Integer saveCodeInt1 = Integer.parseInt(saveCode) - 15;
Integer saveCodeInt2 = Integer.parseInt(saveCode) + 15;
Integer codeInt = Integer.parseInt(code);
if (saveCodeInt1 <= codeInt && saveCodeInt2 >= codeInt) {
redisTemplate.delete(key);
}else{
redisTemplate.delete(key);
throw new ApiException("验证码不合法");
}
}
大概也就这样 有什么问题欢迎来一起共同学习 qq:925259117
更多推荐
所有评论(0)