本文將講解滑動驗證碼由來、原理及功能實現(xiàn)。文章,只貼出主要的邏輯代碼,相關的實現(xiàn)代碼和資源文件可以在項目中獲取。
項目地址:https://gitee.com/gester/captcha.git
同時,推一下字符運算碼和運算驗證碼文章。文章地址:http://www.lxweimin.com/p/fdafd4126c2e
原創(chuàng)不易!如果有幫到您,可以給作者一個小星星鼓勵下 ^ _ ^
滑動驗證碼產生
傳統(tǒng)的字符驗證碼、運算驗證碼已經存在很長一段時間,可以稱得上老古董了,相信每個人都見多。
易用性:在新生滑動驗證碼、點選驗證碼等面前簡直弱爆了。用戶還需要動手、動腦去操作,想想都煩,并且大家都懶嘛,還要照顧近視的同時,和老年用戶,那豈不是有點弱。
安全性:現(xiàn)在已經過度到大數(shù)據(jù)時代,特別是機器學的沖擊。機器通過模板訓練,兩天的時間都可以攻破你的傳統(tǒng)驗證碼。當然滑動驗證碼,點選驗證碼也是可以破解的,相對傳統(tǒng)驗證碼而言,肯定要費力些。
滑動驗證碼原理
- 服務器存有原始圖片、摳圖模板、摳圖邊框等圖片
- 請求獲取驗證碼,服務器隨機獲取一張圖片,根據(jù)摳圖模板圖片在原圖中隨機生成x, y軸的矩形感興趣區(qū)域
- 再通過摳圖模板在感興趣的區(qū)域圖片中摳圖,這里會產生一張小塊的驗證滑塊圖
- 驗證滑塊圖再通過摳圖邊框進行顏色處理,生成帶有描邊的新的驗證滑塊圖
- 原圖再根據(jù)摳圖模板做顏色處理,這里會產生一張遮罩圖(缺少小塊的目標圖)
- 到這里可以得到三張圖,一張原圖,一張遮罩圖。將這三張圖和摳圖的y軸坐標通過base64加密,返回給前端,并將驗證的摳圖位置的x軸、y軸存放在session、db、nosql中
- 前端在移動方塊驗證時,將移動后的x軸和y軸坐標傳遞到后臺與原來的x坐標和y軸坐標作比較,如果在閾值內則驗證通過,驗證通過后可以是給提示或者顯示原圖
- 后端可以通過token、session、redis等方式取出存放的x軸和y軸坐標數(shù)據(jù),與用戶滑動的x軸和y軸進行對比驗證
滑動驗證碼實現(xiàn)
功能
- 滑動驗證碼
- 字符驗證碼(擴展,參見上篇文章)
- 運算驗證碼(擴展,參見上篇文章)
依賴
- 無
實現(xiàn)代碼
獲取驗證碼方法:
/**
* 獲取滑動驗證碼
* @param imageVerificationDto 驗證碼參數(shù)
* @return 滑動驗證碼
* @throws ServiceException 獲取滑動驗證碼異常
*/
public ImageVerificationVo selectSlideVerificationCode(ImageVerificationDto imageVerificationDto) throws ServiceException {
ImageVerificationVo imageVerificationVo = null;
try {
// // 原圖路徑,這種方式不推薦。當運行jar文件的時候,路徑是找不到的,我的路徑是寫到配置文件中的。
// String verifyImagePath = URLDecoder.decode(this.getClass().getResource("/").getPath() + "static/targets", "UTF-8");
// 獲取模板文件,。推薦文件通過流讀取, 因為文件在開發(fā)中的路徑和打成jar中的路徑是不一致的
// InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("static/template/1.png");
File verifyImageImport = new File(verificationImagePathPrefix);
File[] verifyImages = verifyImageImport.listFiles();
Random random = new Random(System.currentTimeMillis());
// 隨機取得原圖文件夾中一張圖片
File originImageFile = verifyImages[random.nextInt(verifyImages.length)];
// 獲取模板圖片文件
File templateImageFile = new File(templateImagePathPrefix + "/template.png");
// 獲取描邊圖片文件
File borderImageFile = new File(templateImagePathPrefix + "/border.png");
// 獲取描邊圖片類型
String borderImageFileType = borderImageFile.getName().substring(borderImageFile.getName().lastIndexOf(".") + 1);
// 獲取原圖文件類型
String originImageFileType = originImageFile.getName().substring(originImageFile.getName().lastIndexOf(".") + 1);
// 獲取模板圖文件類型
String templateImageFileType = templateImageFile.getName().substring(templateImageFile.getName().lastIndexOf(".") + 1);
// 讀取原圖
BufferedImage verificationImage = ImageIO.read(originImageFile);
// 讀取模板圖
BufferedImage readTemplateImage = ImageIO.read(templateImageFile);
// 讀取描邊圖片
BufferedImage borderImage = ImageIO.read(borderImageFile);
// 獲取原圖感興趣區(qū)域坐標
imageVerificationVo = ImageVerificationUtil.generateCutoutCoordinates(verificationImage, readTemplateImage);
int Y = imageVerificationVo.getY();
// 在分布式應用中,可將session改為redis存儲
getRequest().getSession().setAttribute("imageVerificationVo", imageVerificationVo);
// 根據(jù)原圖生成遮罩圖和切塊圖
imageVerificationVo = ImageVerificationUtil.pictureTemplateCutout(originImageFile, originImageFileType, templateImageFile, templateImageFileType, imageVerificationVo.getX(), imageVerificationVo.getY());
// 剪切圖描邊
imageVerificationVo = ImageVerificationUtil.cutoutImageEdge(imageVerificationVo, borderImage, borderImageFileType);
imageVerificationVo.setY(Y);
imageVerificationVo.setType(imageVerificationDto.getType());
// =============================================
// 輸出圖片
// HttpServletResponse response = getResponse();
// response.setContentType("image/jpeg");
// ServletOutputStream outputStream = response.getOutputStream();
// outputStream.write(oriCopyImages);
// BufferedImage bufferedImage = ImageIO.read(originImageFile);
// ImageIO.write(bufferedImage, originImageType, outputStream);
// outputStream.flush();
// =================================================
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
throw new ServiceException(ServiceExceptionCode.URL_DECODER_ERROR);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON);
}
return imageVerificationVo;
}
生成滑動驗證碼調用工具類:
package com.selfimpr.captcha.utils;
import com.selfimpr.captcha.exception.ServiceException;
import com.selfimpr.captcha.exception.code.ServiceExceptionCode;
import com.selfimpr.captcha.model.vo.ImageVerificationVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
/**
* @Description: 圖片驗證工具
* -------------------
* @Author: YangXingfu
* @Date: 2019/07/24 18:40
*/
public class ImageVerificationUtil {
private static final Logger log = LoggerFactory.getLogger(ImageVerificationUtil.class);
// 默認圖片寬度
private static final int DEFAULT_IMAGE_WIDTH = 280;
// 默認圖片高度
private static final int DEFAULT_IMAGE_HEIGHT = 171;
// 獲取request對象
protected static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
// 獲取response對象
protected static HttpServletResponse getResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
/**
* 生成感興趣區(qū)域坐標
* @param verificationImage 源圖
* @param templateImage 模板圖
* @return 裁剪坐標
*/
public static ImageVerificationVo generateCutoutCoordinates(BufferedImage verificationImage, BufferedImage templateImage) {
int X, Y;
ImageVerificationVo imageVerificationVo = null;
// int VERIFICATION_IMAGE_WIDTH = verificationImage.getWidth(); // 原圖寬度
// int VERIFICATION_IMAGE_HEIGHT = verificationImage.getHeight(); // 原圖高度
int TEMPLATE_IMAGE_WIDTH = templateImage.getWidth(); // 摳圖模板寬度
int TEMPLATE_IMAGE_HEIGHT = templateImage.getHeight(); // 摳圖模板高度
Random random = new Random(System.currentTimeMillis());
// 取范圍內坐標數(shù)據(jù),坐標摳圖一定要落在原圖中,否則會導致程序錯誤
X = random.nextInt(DEFAULT_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH) % (DEFAULT_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH + 1) + TEMPLATE_IMAGE_WIDTH;
Y = random.nextInt(DEFAULT_IMAGE_HEIGHT - TEMPLATE_IMAGE_WIDTH) % (DEFAULT_IMAGE_HEIGHT - TEMPLATE_IMAGE_WIDTH - TEMPLATE_IMAGE_WIDTH + 1) + TEMPLATE_IMAGE_WIDTH;
if (TEMPLATE_IMAGE_HEIGHT - DEFAULT_IMAGE_HEIGHT >= 0) {
Y = random.nextInt(10);
}
imageVerificationVo = new ImageVerificationVo();
imageVerificationVo.setX(X);
imageVerificationVo.setY(Y);
return imageVerificationVo;
}
/**
* 根據(jù)模板圖裁剪圖片,生成源圖遮罩圖和裁剪圖
* @param originImageFile 源圖文件
* @param originImageFileType 源圖文件擴展名
* @param templateImageFile 模板圖文件
* @param templateImageFileType 模板圖文件擴展名
* @param X 感興趣區(qū)域X軸
* @param Y 感興趣區(qū)域Y軸
* @return
* @throws ServiceException
*/
public static ImageVerificationVo pictureTemplateCutout(File originImageFile, String originImageFileType, File templateImageFile, String templateImageFileType, int X, int Y) throws ServiceException {
ImageVerificationVo imageVerificationVo = null;
try {
// 讀取模板圖
BufferedImage templateImage = ImageIO.read(templateImageFile);
// 讀取原圖
BufferedImage originImage = ImageIO.read(originImageFile);
int TEMPLATE_IMAGE_WIDTH = templateImage.getWidth();
int TEMPLATE_IMAGE_HEIGHT = templateImage.getHeight();
// 切塊圖 根據(jù)模板圖尺寸創(chuàng)建一張透明圖片
BufferedImage cutoutImage = new BufferedImage(TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT, templateImage.getType());
// 根據(jù)坐標獲取感興趣區(qū)域
BufferedImage interestArea = getInterestArea(X, Y, TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT, originImageFile, originImageFileType);
// 根據(jù)模板圖片切圖
cutoutImage = cutoutImageByTemplateImage(interestArea, templateImage, cutoutImage);
// 圖片繪圖
int bold = 5;
Graphics2D graphics2D = cutoutImage.createGraphics();
graphics2D.setBackground(Color.white);
// 設置抗鋸齒屬性
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setStroke(new BasicStroke(bold, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
graphics2D.drawImage(cutoutImage, 0, 0, null);
graphics2D.dispose();
// 原圖生成遮罩
BufferedImage shadeImage = generateShadeByTemplateImage(originImage, templateImage, X, Y);
imageVerificationVo = new ImageVerificationVo();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 圖片轉為二進制字符串
ImageIO.write(originImage, originImageFileType, byteArrayOutputStream);
byte[] originImageBytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.flush();
byteArrayOutputStream.reset();
// 圖片加密成base64字符串
String originImageString = Base64Utils.encodeToString(originImageBytes);
imageVerificationVo.setOriginImage(originImageString);
ImageIO.write(shadeImage, templateImageFileType, byteArrayOutputStream);
// 圖片轉為二進制字符串
byte[] shadeImageBytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.flush();
byteArrayOutputStream.reset();
// 圖片加密成base64字符串
String shadeImageString = Base64Utils.encodeToString(shadeImageBytes);
imageVerificationVo.setShadeImage(shadeImageString);
ImageIO.write(cutoutImage, templateImageFileType, byteArrayOutputStream);
// 圖片轉為二進制字符串
byte[] cutoutImageBytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.reset();
// 圖片加密成base64字符串
String cutoutImageString = Base64Utils.encodeToString(cutoutImageBytes);
imageVerificationVo.setCutoutImage(cutoutImageString);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON);
}
return imageVerificationVo;
}
/**
* 根據(jù)模板圖生成遮罩圖
* @param originImage 源圖
* @param templateImage 模板圖
* @param x 感興趣區(qū)域X軸
* @param y 感興趣區(qū)域Y軸
* @return 遮罩圖
* @throws IOException 數(shù)據(jù)轉換異常
*/
private static BufferedImage generateShadeByTemplateImage(BufferedImage originImage, BufferedImage templateImage, int x, int y) throws IOException {
// 根據(jù)原圖,創(chuàng)建支持alpha通道的rgb圖片
// BufferedImage shadeImage = new BufferedImage(originImage.getWidth(), originImage.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
BufferedImage shadeImage = new BufferedImage(originImage.getWidth(), originImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
// 原圖片矩陣
int[][] originImageMatrix = getMatrix(originImage);
// 模板圖片矩陣
int[][] templateImageMatrix = getMatrix(templateImage);
// 將原圖的像素拷貝到遮罩圖
for (int i = 0; i < originImageMatrix.length; i++) {
for (int j = 0; j < originImageMatrix[0].length; j++) {
int rgb = originImage.getRGB(i, j);
// 獲取rgb色度
int r = (0xff & rgb);
int g = (0xff & (rgb >> 8));
int b = (0xff & (rgb >> 16));
// 無透明處理
rgb = r + (g << 8) + (b << 16) + (255 << 24);
shadeImage.setRGB(i, j, rgb);
}
}
// 對遮罩圖根據(jù)模板像素進行處理
for (int i = 0; i < templateImageMatrix.length; i++) {
for (int j = 0; j < templateImageMatrix[0].length; j++) {
int rgb = templateImage.getRGB(i, j);
//對源文件備份圖像(x+i,y+j)坐標點進行透明處理
if (rgb != 16777215 && rgb < 0) {
int rgb_ori = shadeImage.getRGB(x + i, y + j);
int r = (0xff & rgb_ori);
int g = (0xff & (rgb_ori >> 8));
int b = (0xff & (rgb_ori >> 16));
rgb_ori = r + (g << 8) + (b << 16) + (140 << 24);
// 對遮罩透明處理
shadeImage.setRGB(x + i, y + j, rgb_ori);
// 設置遮罩顏色
// shadeImage.setRGB(x + i, y + j, rgb_ori);
}
}
}
return shadeImage;
}
/**
* 根據(jù)模板圖摳圖
* @param interestArea 感興趣區(qū)域圖
* @param templateImage 模板圖
* @param cutoutImage 裁剪圖
* @return 裁剪圖
*/
private static BufferedImage cutoutImageByTemplateImage(BufferedImage interestArea, BufferedImage templateImage, BufferedImage cutoutImage) {
// 獲取興趣區(qū)域圖片矩陣
int[][] interestAreaMatrix = getMatrix(interestArea);
// 獲取模板圖片矩陣
int[][] templateImageMatrix = getMatrix(templateImage);
// 將模板圖非透明像素設置到剪切圖中
for (int i = 0; i < templateImageMatrix.length; i++) {
for (int j = 0; j < templateImageMatrix[0].length; j++) {
int rgb = templateImageMatrix[i][j];
if (rgb != 16777215 && rgb < 0) {
cutoutImage.setRGB(i, j, interestArea.getRGB(i, j));
}
}
}
return cutoutImage;
}
/**
* 圖片生成圖像矩陣
* @param bufferedImage 圖片源
* @return 圖片矩陣
*/
private static int[][] getMatrix(BufferedImage bufferedImage) {
int[][] matrix = new int[bufferedImage.getWidth()][bufferedImage.getHeight()];
for (int i = 0; i < bufferedImage.getWidth(); i++) {
for (int j = 0; j < bufferedImage.getHeight(); j++) {
matrix[i][j] = bufferedImage.getRGB(i, j);
}
}
return matrix;
}
/**
* 獲取感興趣區(qū)域
* @param x 感興趣區(qū)域X軸
* @param y 感興趣區(qū)域Y軸
* @param TEMPLATE_IMAGE_WIDTH 模板圖寬度
* @param TEMPLATE_IMAGE_HEIGHT 模板圖高度
* @param originImage 源圖
* @param originImageType 源圖擴展名
* @return
* @throws ServiceException
*/
private static BufferedImage getInterestArea(int x, int y, int TEMPLATE_IMAGE_WIDTH, int TEMPLATE_IMAGE_HEIGHT, File originImage, String originImageType) throws ServiceException {
try {
Iterator<ImageReader> imageReaderIterator = ImageIO.getImageReadersByFormatName(originImageType);
ImageReader imageReader = imageReaderIterator.next();
// 獲取圖片流
ImageInputStream imageInputStream = ImageIO.createImageInputStream(originImage);
// 圖片輸入流順序讀寫
imageReader.setInput(imageInputStream, true);
ImageReadParam imageReadParam = imageReader.getDefaultReadParam();
// 根據(jù)坐標生成矩形
Rectangle rectangle = new Rectangle(x, y, TEMPLATE_IMAGE_WIDTH, TEMPLATE_IMAGE_HEIGHT);
imageReadParam.setSourceRegion(rectangle);
BufferedImage interestImage = imageReader.read(0, imageReadParam);
return interestImage;
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON);
}
}
/**
* 切塊圖描邊
* @param imageVerificationVo 圖片容器
* @param borderImage 描邊圖
* @param borderImageFileType 描邊圖類型
* @return 圖片容器
* @throws ServiceException 圖片描邊異常
*/
public static ImageVerificationVo cutoutImageEdge(ImageVerificationVo imageVerificationVo, BufferedImage borderImage, String borderImageFileType) throws ServiceException{
try {
String cutoutImageString = imageVerificationVo.getCutoutImage();
// 圖片解密成二進制字符創(chuàng)
byte[] bytes = Base64Utils.decodeFromString(cutoutImageString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 讀取圖片
BufferedImage cutoutImage = ImageIO.read(byteArrayInputStream);
// 獲取模板邊框矩陣, 并進行顏色處理
int[][] borderImageMatrix = getMatrix(borderImage);
for (int i = 0; i < borderImageMatrix.length; i++) {
for (int j = 0; j < borderImageMatrix[0].length; j++) {
int rgb = borderImage.getRGB(i, j);
if (rgb < 0) {
cutoutImage.setRGB(i, j , -7237488);
}
}
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(cutoutImage, borderImageFileType, byteArrayOutputStream);
// 新模板圖描邊處理后轉成二進制字符串
byte[] cutoutImageBytes = byteArrayOutputStream.toByteArray();
// 二進制字符串加密成base64字符串
String cutoutImageStr = Base64Utils.encodeToString(cutoutImageBytes);
imageVerificationVo.setCutoutImage(cutoutImageStr);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON);
}
return imageVerificationVo;
}
}
滑動驗證碼驗證方法:
/**
* 滑動驗證碼驗證方法
* @param X x軸坐標
* @param Y y軸坐標
* @return 滑動驗證碼驗證狀態(tài)
* @throws ServiceException 驗證滑動驗證碼異常
*/
@Override
public boolean checkVerificationResult(String X, String Y) throws ServiceException {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
ImageVerificationVo imageVerificationVo = (ImageVerificationVo) request.getSession().getAttribute("imageVerificationVo");
if (imageVerificationVo != null) {
if ((Math.abs(Integer.parseInt(X) - imageVerificationVo.getX()) <= 5) && Y.equals(String.valueOf(imageVerificationVo.getY()))) {
System.out.println("驗證成功");
return true;
} else {
System.out.println("驗證失敗");
return false;
}
} else {
return false;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(ServiceExceptionCode.IO_EXCEPTON);
}
}
預覽圖
在這里插入圖片描述
后話
以上部分為主要業(yè)務邏輯代碼,你需要創(chuàng)建一個類和簡單的調試一下就能正常運行使用。相關的圖片資源文件和模板文件參見項目地址:https://gitee.com/gester/captcha.git
同時,推薦一波字符驗證碼和運算驗證碼文章。文章地址:http://www.lxweimin.com/p/fdafd4126c2e
如果這篇文章有幫助到您,請給一個star,謝謝大大。