背景:
發送短信驗證碼或者登錄等場景操作之前都需要進行圖片驗證碼校驗或者滑塊驗證碼校驗;此舉是爲了減少黑盒對服務端進行暴力破解密碼或者頻發短信轟炸請求的操作;
但如果滑塊驗證完全由前端進行操作,實際上是不能很好的進行黑盒測試的防禦,很容易繞過,所以在這裏推出一個前後端結合滑塊驗證碼的實例;
步驟:
- 後臺生成背景圖+模版摳圖+摳圖橫座標+摳圖縱座標;
- 後臺生成流水號,保存摳圖的橫座標到內存或者redis等,返回前端背景圖+模版摳圖+摳圖縱座標;
- 前端根據背景圖+模版摳圖+摳圖縱座標進行滑塊插件展示;
- 用戶操作成功後傳輸流水號+滑塊橫座標到後臺,後臺進行比較是否在誤差範圍內(比如5像素)返回前端顯示;
效果圖:
源碼解析:
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 = '<div class="code-k-div">'
+ '<div class="code_bg"></div>'
+ '<div class="code-con">'
+ '<div class="code-img">'
+ '<div class="code-img-con">'
+ '<div class="code-mask"><img class="code-front-img" src="'
+ opts.frontimg
+ '"></div>'
+ '<img class="code-back-img" src="'
+ opts.backimg
+ '"></div>'
+ '<div class="code-push"><i class="icon-login-bg icon-w-25 icon-push">刷新</i><span class="code-tip"></span></div>'
+ '</div>' + '<div class="code-btn">'
+ '<div class="code-btn-img code-btn-m"></div>'
+ '<span class="code-span">按住滑塊,拖動完成上方拼圖</span>'
+ '</div></div></div>';
$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.前端結構及業務實踐:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>滑動驗證碼</title>
<link rel="stylesheet" href="/css/slide.css">
<script src="/js/jquery-1.11.1.min.js"></script>
<script src="/js/jquery.lgyslide.js"></script>
</head>
<body>
<div id="imgscode"></div>
<script>
$(function() {
setTimeout(function() {
createcode();
}, 1000)
}());
//顯示驗證碼
function createcode() {
$
.ajax({
type : 'POST',
url : '/getPic',
dataType : 'json',
success : function(data) {
if (data != null) {
$("#imgscode")
.imgcode(
{
frontimg : 'data:image/png;base64,'
+ data.slidingImage,
backimg : 'data:image/png;base64,'
+ data.backImage,
yHeight : data.yHeight,
refreshcallback : function() {
//刷新驗證碼
createcode();
},
callback : function(msg) {
console.log(msg);
var $this = this;
$
.ajax({
type : 'POST',
url : '/checkcapcode',
data : {
xpos : msg.xpos,
capcode : data.capcode
},
dataType : 'json',
success : function(
data) {
console
.log(data)
if (data.code == 1) {
$this
.getsuccess();
setTimeout(
function() {
alert("驗證成功,可以做自己的操作了!");
},
800);
} else {
if (data.code == 4) {
createcode();
} else if (data.code == 3) {
$this
.getfail("驗證碼過期,請刷新");
} else {
$this
.getfail("驗證不通過");
}
}
}
})
}
});
}
}
})
}
</script>
</body>
</html>
5.訪問http://localhost:8080/index;效果如上所述;