前言
在实际的登陆场景或者注册场景中我们总是可以见到各式各样的验证码,有四位的数字字母验证码,滑块验证码,算数验证码,短信验证码,还有类似于12306的逆天验证码。验证码的生成与校验可在前端也可以在后端,但是为了安全考虑,一般将验证码的的生成与校验放置在后台,防止被暴力破解。这里先介绍一波图片的滑块验证:
准备工作:
六张像素为290*147的图片
思路:
- 后台获取源图片(准备的六张图片),获取随机坐标来截取滑块图片(此处坐标y坐标写死50,x坐标随机),根据滑块图片轮廓将源图片该区域设置阴影获取阴影图片。包括X轴随机坐标轴在内,此处一共获取了四个元素。
- 将滑块图片,阴影图片,源图片,返回前端用于滑块验证,将X轴位置存session,用于校验
- 前端生成ui,并获取拖动滑块时的位移距离,将位移返回后台与session中的x轴位置做对比,比较值小于容差则通过,否则失败。
说白了,其实也就是比较了滑块的位移距离与源图片生成滑块的X轴位置,当然这是一种最基本的校验方式,高级一点,你可以获取滑块移动过程的路线轨迹,时间的因素用于校验是否真人操作,本人水平有限。
然后结合具体代码来一波实战
工具类:生成了思路中所说的四个元素并封装为了对象:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Random;
/**
* 滑块验证码工具类
* Copyright (c) 2019 Choice, Inc.
* All Rights Reserved.
* Choice Proprietary and Confidential.
*
* @author duhuo
* @since 2019/8/22 19:51
*/
public class SliderCheckUtil {
private static final Logger logger = LoggerFactory.getLogger(SliderCheckUtil.class);
static int targetLength=55;//小图长
static int targetWidth=45;//小图宽
static int circleR=6;//半径
static int r1=3;//距离点
/* public static void main(String[] args) throws Exception{
int[][] blockData = getBlockData();
BufferedImage resourceImg = ImageIO.read(new File("D://ver-3.png"));
//BufferedImage target = ImageIO.read(new File("D://ver-3.png"));
BufferedImage target= new BufferedImage(targetLength, targetWidth, BufferedImage.TYPE_4BYTE_ABGR);
cutByTemplate(resourceImg, target, blockData, 250, 50);
String imageBASE64 = getImageBASE64(resourceImg);
String imageBASE641 = getImageBASE64(target);
System.out.println(2222);
}*/
public static SliderCheck build() {
try {
int max = 227;
int min = 100;
int x = new Random().nextInt(max - min) + min;
// 生成base64
int[][] blockData = getBlockData();
int number = new Random().nextInt(6) + 1;
BufferedImage resourceImg = ImageIO.read(new File("D:\\toux\\v2\\"+number+".png"));
String resourceImgSt = getImageBASE64(resourceImg);
BufferedImage puzzleImg = new BufferedImage(targetLength, targetWidth, BufferedImage.TYPE_4BYTE_ABGR);
cutByTemplate(resourceImg, puzzleImg, blockData, x, 50);
String resourceUpImgSt = getImageBASE64(resourceImg);
String puzzleImgSt = getImageBASE64(puzzleImg);
SliderCheck sliderCheck = new SliderCheck();
sliderCheck.setSourceImg(resourceImgSt);
sliderCheck.setImgWidth("290");
sliderCheck.setImgHeight("147");
sliderCheck.setModifyImg(resourceUpImgSt);
sliderCheck.setPuzzleImg(puzzleImgSt);
sliderCheck.setPuzzleWidth(targetLength + "");
sliderCheck.setPuzzleHeight(targetWidth + "");
sliderCheck.setPuzzleYAxis("50");
sliderCheck.setPuzzleXAxis(x + "");
return sliderCheck;
} catch (Exception e) {
logger.error("滑块", e);
return null;
}
}
public static String getImageBASE64(BufferedImage image) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image,"png",out);
byte[] b = out.toByteArray();//转成byte数组
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(b);//生成base64编码
}
/**
*
* @Createdate: 2019年1月24日上午10:52:42
* @Title: getBlockData
* @Description: 生成小图轮廓
* @author mzl
* @return int[][]
* @throws
*/
private static int[][] getBlockData() {
int[][] data = new int[targetLength][targetWidth];
double x2 = targetLength-circleR;
//随机生成圆的位置
double h1 = circleR + Math.random() * (targetWidth-3*circleR-r1);
double po = circleR*circleR;
double xbegin = targetLength-circleR-r1;
double ybegin = targetWidth-circleR-r1;
for (int i = 0; i < targetLength; i++) {
for (int j = 0; j < targetWidth; j++) {
double d3 = Math.pow(i - x2,2) + Math.pow(j - h1,2);
double d2 = Math.pow(j-2,2) + Math.pow(i - h1,2);
if ((j <= ybegin && d2 <= po)||(i >= xbegin && d3 >= po)) {
data[i][j] = 0;
} else {
data[i][j] = 1;
}
}
}
return data;
}
/**
*
* @Createdate: 2019年1月24日上午10:51:30
* @Title: cutByTemplate
* @Description: 生成小图片、给大图片添加阴影
* @author mzl
* @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){
for (int i = 0; i < targetLength; i++) {
for (int j = 0; j < targetWidth; j++) {
int rgb = templateImage[i][j];
// 原图中对应位置变色处理
int rgb_ori = oriImage.getRGB(x + i, y + j);
if (rgb == 1) {
//抠图上复制对应颜色值
targetImage.setRGB(i, j, rgb_ori);
//原图对应位置颜色变化
oriImage.setRGB(x + i, y + j, rgb_ori & 0x363636 );
}else{
//这里把背景设为透明
targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
}
}
}
}
public static class SliderCheck {
// 原图
private String sourceImg;
private String imgWidth;
private String imgHeight;
// 扣过图的图片
private String modifyImg;
// 拼图
private String puzzleImg;
private String puzzleWidth;
private String puzzleHeight;
// 坐标
private String puzzleYAxis;
private String puzzleXAxis;
public String getSourceImg() {
return sourceImg;
}
public void setSourceImg(String sourceImg) {
this.sourceImg = sourceImg;
}
public String getImgWidth() {
return imgWidth;
}
public void setImgWidth(String imgWidth) {
this.imgWidth = imgWidth;
}
public String getImgHeight() {
return imgHeight;
}
public void setImgHeight(String imgHeight) {
this.imgHeight = imgHeight;
}
public String getModifyImg() {
return modifyImg;
}
public void setModifyImg(String modifyImg) {
this.modifyImg = modifyImg;
}
public String getPuzzleImg() {
return puzzleImg;
}
public void setPuzzleImg(String puzzleImg) {
this.puzzleImg = puzzleImg;
}
public String getPuzzleWidth() {
return puzzleWidth;
}
public void setPuzzleWidth(String puzzleWidth) {
this.puzzleWidth = puzzleWidth;
}
public String getPuzzleHeight() {
return puzzleHeight;
}
public void setPuzzleHeight(String puzzleHeight) {
this.puzzleHeight = puzzleHeight;
}
public String getPuzzleYAxis() {
return puzzleYAxis;
}
public void setPuzzleYAxis(String puzzleYAxis) {
this.puzzleYAxis = puzzleYAxis;
}
public String getPuzzleXAxis() {
return puzzleXAxis;
}
public void setPuzzleXAxis(String puzzleXAxis) {
this.puzzleXAxis = puzzleXAxis;
}
}
}
controller,此处包含了图片验证码获取与验证的方法,登录验证代码用
/**
* 获取验证码资源
* @Author qnz
* @Date 2019/8/27 10:49
* @Param [request]
* @return com.payment.platform.v2.common.bean.vo.ApiResult
**/
@GetMapping("/get-slider-img")
public ApiResult getSliderImg(HttpServletRequest request){
SliderCheckUtil.SliderCheck build = SliderCheckUtil.build();
request.getSession().setAttribute("slider-X-index",build.getPuzzleXAxis());
build.setPuzzleXAxis("");
return ApiResult.success(build, "图片验证加载成功");
}
/**
* 滑块验证
* @Author qnz
* @Date 2019/8/27 10:49
* @Param [request, distance]
* @return com.payment.platform.v2.common.bean.vo.ApiResult
**/
@PostMapping("/slider-check")
public ApiResult sliserCheck(HttpServletRequest request, Long distance){
String index = (String)request.getSession().getAttribute("slider-X-index");
if (distance >= (Long.valueOf(index)-5) && distance <= (Long.valueOf(index) + 5)){
return ApiResult.success(null,"验证成功");
}
return ApiResult.error("验证失败");
}
js文件:
function imgVer(Config) {
// 初始化
var el = eval(Config.el); // 验证区域
var w = Config.width; // 区域宽
var h = Config.height; // 区域高
// 设置拼图缺失坐标取值范围
var PL_Size = 34;//**缺失拼图的大小
var padding = 20;//缺失拼图与边框的距离
// 拼图初始化存在的left位置
var html = '<div style="position:relative;padding:16px 16px 28px;border:1px solid #ddd;background:#f2ece1;border-radius:16px;">';
html += '<div style="position:relative;overflow:hidden;width:' + w + 'px;">';
html += '<div style="position:relative;width:' + w + 'px;height:' + h + 'px;">';
html += '<img id="scream" src="' + '' + '" style="width:' + w + 'px;height:' + h + 'px;">';
//html += '<canvas id="puzzleBox" width="'+ w +'" height="'+ h +'" style="position:absolute;left:0;top:0;z-index:22;"></canvas>';
html += '<img id="_pt" width="55" height="45" style="position:absolute;left:0;top:0;z-index:22;"></img>';
html += '</div>';
html += '<div class="puzzle-lost-box" style="position:absolute;width:' + w + 'px;height:' + h + 'px;top:0;left:' + 0 + 'px;z-index:111;">';
html += '<canvas id="puzzleShadow" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:22;"></canvas>';
html += '<canvas id="puzzleLost" width="' + w + '" height="' + h + '" style="position:absolute;left:0;top:0;z-index:33;"></canvas>';
html += '</div>';
html += '<p class="ver-tips"></p>';
html += '</div>';
html += '<div class="re-btn"><a></a></div>';
html += '</div>';
html += '<br>';
html += '<div style="position:relative;width:' + w + 'px;margin:auto;">';
html += '<div style="border:1px solid #c3c3c3;border-radius:24px;background:#ece4dd;box-shadow:0 1px 1px rgba(12,10,10,0.2) inset;">';
html += '<p style="font-size:12px;color: #486c80;line-height:28px;margin:0;text-align:right;padding-right:22px;">按住左边滑块,拖动完成上方拼图</p>';
html += '</div>';
html += '<div class="slider-btn"></div>';
html += '</div>';
// 动态加载滑块验证区域
el.html(html);
// 获取验证码资源
$.get("/get-slider-img", function(data){
if(data.success == true){
// 扣过的背景图
document.getElementById("scream").src = "data:image/png;base64," + data.data.modifyImg;
// 拼图
document.getElementById("_pt").src = "data:image/png;base64," + data.data.puzzleImg;
// 设置滑块在阴影图中的高度
$("#_pt").css("top", "50px");
}else{
layer.msg("资源加载失败");
}
},"json");
// 滑块拖动
var moveStart = '';//定义一个鼠标按下的X轴值
//* 鼠标按下 / 手指按下
$(".slider-btn").on('mousedown touchstart',function (e) {
e = e || window.event;
// 鼠标在滑块按下切换滑块背景
$(this).css({
"background-position":"0 -216px"
});
moveStart = e.pageX || e.originalEvent.targetTouches[0].pageX;//记录鼠标按下时的坐标 X轴值
});
//* 鼠标拖动 / 手指滑动
$(window).on('mousemove touchmove',function (e) {
e = e || window.event;
var moveX = e.pageX || e.originalEvent.targetTouches[0].pageX;//监听鼠标的位置
var d = moveX-moveStart;//鼠标按住后在X轴上移动的距离
if(moveStart == '') {
// console.log('未拖动滑块');
} else {
if(d < 0 || d > (w-padding-PL_Size)) {
// console.log('超过范围');
} else {
$(".slider-btn").css({
"left":d + 'px',
"transition":"inherit"
});
$("#puzzleLost").css({
"left":d + 'px',
"transition":"inherit"
});
$("#puzzleShadow").css({
"left":d + 'px',
"transition":"inherit"
});
$("#_pt").css({
"left":d + 'px',
"transition":"inherit"
});
}
}
})
//* 鼠标松开 / 手指离开
$(window).on('mouseup touchend',function (e) {
e = e || window.event;
var e_pageX = e.originalEvent.changedTouches == undefined?moveStart:e.originalEvent.changedTouches[0];
var moveEnd_X = e.pageX - moveStart || e_pageX.pageX - moveStart;//松开鼠标后滑块移动的距离
// 未拖动滑块将不发生校验请求
if (moveStart == null || moveStart == ""){
return ;
}
$.post("/slider-check", {distance: moveEnd_X}, function(data){
if(data.success == true){
$(".ver-tips").html('<i style="background-position:-4px -1207px;"></i><span style="color:#42ca6b;">验证通过</span><span></span>');
$(".ver-tips").addClass("slider-tips");
$(".puzzle-lost-box").addClass("hidden");
$("#puzzleBox").addClass("hidden");
setTimeout(function () {
$(".ver-tips").removeClass("slider-tips");
// imgVer(Config);
},2000);
document.getElementById("scream").src = "/assets/module/slider-img/images/pass2.png";
$("#_pt").hide();
Config.success();
}else{
$(".ver-tips").html('<i style="background-position:-4px -1229px;"></i><span style="color:red;">验证失败:</span><span style="margin-left:4px;">拖动滑块将悬浮图像正确拼合</span>');
$(".ver-tips").addClass("slider-tips");
setTimeout(function () {
$(".ver-tips").removeClass("slider-tips");
},2000);
Config.error();
}
},"json");
setTimeout(function () {
$(".slider-btn").css({
"left":'0',
"transition":"left 0.5s"
});
$("#puzzleLost").css({
"left":'0',
"transition":"left 0.5s"
});
$("#puzzleShadow").css({
"left":'0',
"transition":"left 0.5s"
});
$("#_pt").css({
"left":'0',
"transition":"left 0.5s"
});
},1000);
$(".slider-btn").css({
"background-position":"0 -84px"
});
moveStart = '';// 清空上一次鼠标按下时的坐标X轴值;
})
$(".re-btn a").on("click",function () {
imgVer(Config);
})
setTimeout(function () {
$(".slider-btn").css({
"left":'0',
"transition":"left 0.5s"
});
$("#puzzleLost").css({
"left":'0',
"transition":"left 0.5s"
});
$("#puzzleShadow").css({
"left":'0',
"transition":"left 0.5s"
});
$("#_pt").css({
"left":'0',
"transition":"left 0.5s"
});
},1000);
}
html,此处需引入 jquery.min.js,并设置相关css
在html中添加验证div:
<div class="layui-form-item">
<div class="verBox">
<div id="imgVer" style="display:inline-block;"></div>
</div>
</div>
在html中添加js
var loginFlag = false;
// 获取滑块相关资源
$(function() {
getSliderImg();
})
// 初始化验证码
function getSliderImg() {
imgVer({
el:'$("#imgVer")',
width:'290',
height:'147',
success:function () {
loginFlag = true;
},
error:function () {
loginFlag = false;
}
});
}
此处的资源文件要替换成自己的目录,该文件放在最后了。
<style>
.slider-btn {
position:absolute;
width:44px;
height:44px;
left:0;
top:-7px;
z-index:12;
cursor:pointer;
background-image:url("/assets/module/slider-img/images/sprite.3.2.0.png");
background-position:0 -84px;
transition:inherit;
}
.ver-tips i {
display:inline-block;
width:22px;
height:22px;
vertical-align:top;
background-image: url("/assets/module/slider-img/images/sprite.3.2.0.png");
background-position: -4px -1229px;
}
.re-btn a {
display:inline-block;
width:14px;
height:14px;
margin:7px 0;
background-image: url("/assets/module/slider-img/images/sprite.3.2.0.png");
background-position: 0 -1179px;
cursor:pointer;
}
.verBox {
width:100%;
text-align:center;
left:404px;
top:0;
opacity:1;
transition:all 0.8s;
padding-top:30px;
}
.ver-tips {
position:absolute;
left:0;
bottom:-22px;
background:rgba(255,255,255,0.9);
height:22px;
line-height:22px;
font-size:12px;
width:100%;
margin:0;
text-align:left;
padding:0 8px;
transition:all 0.4s;
}
.slider-tips {
bottom:0;
}
.ver-tips span {
display:inline-block;
vertical-align:top;
line-height:22px;
color:#455;
}
.active-tips {
display:block;
}
.hidden {
display:none;
}
.re-btn {
position:absolute;
left:0;
bottom:0;
height:28px;
padding:0 16px;
}
.re-btn a:hover {
background-position: 0 -1193px;
}
</style>
效果图:
资源文件: