发送短信验证码或者登录等场景操作之前都需要进行图片验证码校验或者滑块验证码校验;此举是为了减少黑盒对服务端进行暴力破解密码或者频发短信轰炸请求的操作;
但如果滑块验证完全由前端进行操作,实际上是不能很好的进行黑盒测试的防御,很容易绕过,所以在这里推出一个前后端结合滑块验证码的实例;
1、后端java生成滑块图片工具类,传入背景图及抠图模版进行抠图返回背景图+模版抠图+抠图横坐标+抠图纵坐标;
package cn.cc2gjx.sliderverificationcode.sliding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
*
* @模块名:javaslidingverification
* @包名:com.liangyt.javaslidingverification.sliding
* @类名称: VerifyImageUtil
* @类描述:【类描述】滑块验证码生成工具类
* @版本:1.0
* @创建人:cc
* @创建时间:2019年10月24日上午10:11:22
*/
public class VerifyImageUtil {
private static Logger log = LoggerFactory.getLogger(VerifyImageUtil.class);
private static int BOLD = 5;
private static final String IMG_FILE_TYPE = "jpg";
private static final String TEMP_IMG_FILE_TYPE = "png";
/**
* 根据模板切图
*
* @param templateFile
* @param targetFile
* @return
* @throws Exception
*/
public static Map < String, Object > pictureTemplatesCut(File templateFile, File targetFile) throws Exception {
Map < String, Object > pictureMap = new HashMap <>();
// 模板图
BufferedImage imageTemplate = ImageIO.read(templateFile);
int templateWidth = imageTemplate.getWidth();
int templateHeight = imageTemplate.getHeight();
// 原图
BufferedImage oriImage = ImageIO.read(targetFile);
int oriImageWidth = oriImage.getWidth();
int oriImageHeight = oriImage.getHeight();
// 随机生成抠图坐标X,Y
// X轴距离右端targetWidth Y轴距离底部targetHeight以上
Random random = new Random();
int widthRandom = random.nextInt(oriImageWidth - 2 * templateWidth) + templateWidth;
// int heightRandom = 1;
int heightRandom = random.nextInt(oriImageHeight - templateHeight);
log.info("原图大小{} x {},随机生成的坐标 X,Y 为({},{})", oriImageWidth, oriImageHeight, widthRandom, heightRandom);
// 新建一个和模板一样大小的图像,TYPE_4BYTE_ABGR表示具有8位RGBA颜色分量的图像,正常取imageTemplate.getType()
BufferedImage newImage = new BufferedImage(templateWidth, templateHeight, imageTemplate.getType());
// 得到画笔对象
Graphics2D graphics = newImage.createGraphics();
// 如果需要生成RGB格式,需要做如下配置,Transparency 设置透明
newImage = graphics.getDeviceConfiguration().createCompatibleImage(templateWidth, templateHeight,
Transparency.TRANSLUCENT);
// 新建的图像根据模板颜色赋值,源图生成遮罩
cutByTemplate(oriImage, imageTemplate, newImage, widthRandom, heightRandom);
// 设置“抗锯齿”的属性
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setStroke(new BasicStroke(BOLD, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
graphics.drawImage(newImage, 0, 0, null);
graphics.dispose();
ByteArrayOutputStream newImageOs = new ByteArrayOutputStream();// 新建流。
ImageIO.write(newImage, TEMP_IMG_FILE_TYPE, newImageOs);// 利用ImageIO类提供的write方法,将bi以png图片的数据模式写入流。
byte[] newImagebyte = newImageOs.toByteArray();
ByteArrayOutputStream oriImagesOs = new ByteArrayOutputStream();// 新建流。
ImageIO.write(oriImage, IMG_FILE_TYPE, oriImagesOs);// 利用ImageIO类提供的write方法,将bi以jpg图片的数据模式写入流。
byte[] oriImageByte = oriImagesOs.toByteArray();
pictureMap.put("slidingImage", Base64Utils.encodeToString(newImagebyte));
pictureMap.put("backImage", Base64Utils.encodeToString(oriImageByte));
pictureMap.put("xWidth", widthRandom);
pictureMap.put("yHeight", heightRandom);
return pictureMap;
}
/**
* 添加水印
*
* @param oriImage
*/
/*
* private static BufferedImage addWatermark(BufferedImage oriImage) throws IOException { Graphics2D graphics2D =
* oriImage.createGraphics(); graphics2D .setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints
* .VALUE_INTERPOLATION_BILINEAR); // 设置水印文字颜色 graphics2D.setColor(Color.BLUE); // 设置水印文字Font graphics2D.setFont(new
* java.awt.Font("宋体", java.awt.Font.BOLD, 50)); // 设置水印文字透明度 graphics2D.setComposite
* (AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f)); // 第一参数->设置的内容,后面两个参数->文字在图片上的坐标位置(x,y)
* graphics2D.drawString("[email protected]", 400,300); graphics2D.dispose(); //释放 return oriImage; }
*/
/**
* @param oriImage 原图
* @param templateImage 模板图
* @param newImage 新抠出的小图
* @param x 随机扣取坐标X
* @param y 随机扣取坐标y
* @throws Exception
*/
private static void cutByTemplate(BufferedImage oriImage, BufferedImage templateImage, BufferedImage newImage,
int x, int y) {
// 临时数组遍历用于高斯模糊存周边像素值
int[][] martrix = new int[3][3];
int[] values = new int[9];
int xLength = templateImage.getWidth();
int yLength = templateImage.getHeight();
// 模板图像宽度
for (int i = 0; i < xLength; i++) {
// 模板图片高度
for (int j = 0; j < yLength; j++) {
// 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
int rgb = templateImage.getRGB(i, j);
if (rgb < 0) {
newImage.setRGB(i, j, oriImage.getRGB(x + i, y + j));
// 抠图区域高斯模糊
readPixel(oriImage, x + i, y + j, values);
fillMatrix(martrix, values);
oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
}
// 防止数组越界判断
if (i == (xLength - 1) || j == (yLength - 1)) {
continue;
}
int rightRgb = templateImage.getRGB(i + 1, j);
int downRgb = templateImage.getRGB(i, j + 1);
// 描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
if ((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0)
|| (rgb < 0 && downRgb >= 0)) {
newImage.setRGB(i, j, Color.white.getRGB());
oriImage.setRGB(x + i, y + j, Color.white.getRGB());
}
}
}
}
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();
}
public static void main(String[] args) {
}
}
2、进行后台编码,主要有生成滑块并保存横坐标的getPic()和进行校验的checkcapcode()方法;
package cn.cc2gjx.sliderverificationcode.controller;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import cn.cc2gjx.sliderverificationcode.sliding.VerifyImageUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
*
* @模块名:javaslidingverification
* @包名:com.liangyt.javaslidingverification.controller
* @类名称: SliderIMageController
* @类描述:【类描述】滑块验证码控制层
* @版本:1.0
* @创建人:cc
* @创建时间:2019年10月24日上午10:44:30
*/
@Controller
public class SliderIMageController {
// 保存横轴位置用于对比,并设置最大数量为10000,多了就先进先出,并设置超时时间为70秒
public static Cache < String, Integer > cacheg = CacheBuilder.newBuilder().expireAfterWrite(70, TimeUnit.SECONDS)
.maximumSize(10000).build();
@GetMapping
@RequestMapping("index")
public String test(HttpServletRequest request, Model model) throws IOException {
return "index";
}
@GetMapping
@RequestMapping("getPic")
public @ResponseBody Map < String, Object > getPic(HttpServletRequest request) throws IOException {
// 读取图库目录
File imgCatalog = new File(ResourceUtils.getURL("classpath:").getPath() + "sliderimage\\targets\\");
File[] files = imgCatalog.listFiles();
// 随机选择需要切的图
int randNum = new Random().nextInt(files.length);
File targetFile = files[randNum];
// 随机选择剪切模版
Random r = new Random();
int num = r.nextInt(6) + 1;
File tempImgFile = new File(ResourceUtils.getURL("classpath:").getPath() + "sliderimage\\templates\\" + num
+ "-w.png");
// 根据模板裁剪图片
try {
Map < String, Object > resultMap = VerifyImageUtil.pictureTemplatesCut(tempImgFile, targetFile);
// 生成流水号,这里就使用时间戳代替
String lno = Calendar.getInstance().getTimeInMillis() + "";
cacheg.put(lno, Integer.valueOf(resultMap.get("xWidth") + ""));
resultMap.put("capcode", lno);
// 移除横坐标送前端
resultMap.remove("xWidth");
return resultMap;
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}
@GetMapping
@RequestMapping("checkcapcode")
public @ResponseBody Map < String, Object > checkcapcode(@RequestParam("xpos") int xpos,
@RequestParam("capcode") String capcode, HttpServletRequest request) throws IOException {
Map < String, Object > result = new HashMap < String, Object >();
Integer x = cacheg.getIfPresent(capcode);
if (x == null) {
// 超期
result.put("code", 3);
}
else if (xpos - x > 5 || xpos - x < -5) {
// 验证失败
result.put("code", 2);
}
else {
// 验证成功
result.put("code", 1);
// .....做自己的操作,发送验证码
}
return result;
}
}
3、前端关键插件代码,和网上的有所差别,添加了纵轴的变更:
/**
* Created by lgy on 2017/10/21. 图片验证码
*/
(function($) {
$.fn.imgcode = function(options) {
// 初始化参数
var defaults = {
frontimg : "",
backimg : "",
refreshImg : "",
getsuccess : "",
getfail : "",
maskclose : true,
callback : "", // 回调函数
refreshcallback : "",
yHeight : 1
};
var opts = $.extend(defaults, options);
return this
.each(function() {
var $this = $(this);// 获取当前对象
var html = ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ ''
+ '刷新'
+ '' + ''
+ ''
+ '按住滑块,拖动完成上方拼图'
+ '';
$this.html(html);
$(".code-mask").css("margin-top",opts.yHeight+"px")
// 定义拖动参数
var $divMove = $(this).find(".code-btn-img"); // 拖动按钮
var $divWrap = $(this).find(".code-btn");// 鼠标可拖拽区域
var mX = 0, mY = 0;// 定义鼠标X轴Y轴
var dX = 0, dY = 0;// 定义滑动区域左、上位置
var isDown = false;// mousedown标记
if (document.attachEvent) {// ie的事件监听,拖拽div时禁止选中内容,firefox与chrome已在css中设置过-moz-user-select:
// none; -webkit-user-select:
// none;
$divMove[0].attachEvent('onselectstart', function() {
return false;
});
}
// 按钮拖动事件
$divMove.unbind('mousedown').on({
mousedown : function(e) {
// 清除提示信息
$this.find(".code-tip").html("");
var event = e || window.event;
mX = event.pageX;
dX = $divWrap.offset().left;
dY = $divWrap.offset().top;
isDown = true;// 鼠标拖拽启
$(this).addClass("active");
// 修改按钮阴影
$divMove.css({
"box-shadow" : "0 0 8px #666"
});
}
});
// 点击背景关闭
if (opts.maskclose) {
$this.find(".code_bg").unbind('click').click(
function() {
$this.html("");
})
}
// 刷新code码
$this.find(".icon-push").unbind('click').click(function() {
opts.refreshcallback();
});
// 鼠标点击松手事件
$divMove.unbind('mouseup')
.mouseup(
function(e) {
var lastX = $this.find(".code-mask")
.offset().left
- dX - 1;
isDown = false;// 鼠标拖拽启
$divMove.removeClass("active");
// 还原按钮阴影
$divMove.css({
"box-shadow" : "0 0 3px #ccc"
});
returncode(lastX);
});
// 滑动事件
$divWrap
.mousemove(function(event) {
var event = event || window.event;
var x = event.pageX;// 鼠标滑动时的X轴
if (isDown) {
if (x > (dX + 30)
&& x < dX + $(this).width() - 20) {
$divMove.css({
"left" : (x - dX - 20) + "px"
});// div动态位置赋值
$this.find(".code-mask").css({
"left" : (x - dX - 30) + "px"
});
}
}
});
// 返回坐标系
function returncode(xpos) {
opts.callback({
xpos : xpos
});
}
// 验证数据
function checkcode(code) {
var iscur = true;
// 模拟ajax
setTimeout(function() {
if (iscur) {
checkcoderesult(1, "验证通过");
$this.find(".code-k-div").remove();
opts.callback({
code : 1000,
msg : "验证通过",
msgcode : "23dfdf123"
});
} else {
$divMove.addClass("error");
checkcoderesult(0, "验证不通过");
opts.callback({
code : 1001,
msg : "验证不通过"
});
setTimeout(function() {
$divMove.removeClass("error");
$this.find(".code-mask").animate({
"left" : "0px"
}, 200);
$divMove.animate({
"left" : "10px"
}, 200);
}, 300);
}
}, 500)
}
// 刷新图标
opts.refreshImg = function(data) {
console.log(data)
$this.find(".code-img-con .code-front-img").attr("src",
data.frontImg);
$this.find(".code-img-con .code-back-img").attr("src",
data.backGoundImg);
}
// 验证成功
opts.getsuccess = function() {
checkcoderesult(1, "验证通过");
setTimeout(function() {
$this.find(".code-k-div").remove();
}, 800);
}
// 验证失败
opts.getfail = function(txt) {
$divMove.addClass("error");
checkcoderesult(0, txt);
setTimeout(function() {
$divMove.removeClass("error");
$this.find(".code-mask").animate({
"left" : "0px"
}, 200);
$divMove.animate({
"left" : "10px"
}, 200);
}, 400);
}
// 验证结果
function checkcoderesult(i, txt) {
if (i == 0) {
$this.find(".code-tip").addClass("code-tip-red");
} else {
$this.find(".code-tip").addClass("code-tip-green");
}
$this.find(".code-tip").html(txt);
}
})
}
})(jQuery);
4.前端结构及业务实践:
滑动验证码
5.访问http://localhost:8080/index;效果如上所述;
https://github.com/cc6688211/sliderverificationcode.git