最近有需求要实现拼图滑动验证码,找了点资料做了个demo看看。
简单来说就是后端随机获取图片,随即切割后把处理完的两张图片,拼图和背景存放到对应文件夹,之后将文件地址(名称)和拼图位置信息返回到前端,实现拖动重合效果即可。
由bean类进行数据存放,包括拼图左上角的x,y坐标位置,拼图和背景文件名称。
public class VerificationCodePlace {
private String backName;
private String markName;
private int xLocation;
private int yLocation;
public VerificationCodePlace(){}
public VerificationCodePlace(String backName, String markName, int xLocation, int yLocation){
this.backName = backName;
this.markName = markName;
this.xLocation = xLocation;
this.yLocation = yLocation;
}
public String getBackName() {
return backName;
}
public void setBackName(String backName) {
this.backName = backName;
}
public String getMarkName() {
return markName;
}
public void setMarkName(String markName) {
this.markName = markName;
}
public int getxLocation() {
return xLocation;
}
public void setxLocation(int xLocation) {
this.xLocation = xLocation;
}
public int getyLocation() {
return yLocation;
}
public void setyLocation(int yLocation) {
this.yLocation = yLocation;
}
}
该工具类的随机切割方法主要参考https://www.jianshu.com/p/eb190c26fb5b中提供的方法,利用一个图片宽×图片高的数组来存放像素透明,保持原状或者半透明的信息,来实现拼图造型的随机生成。
public class VerificationCodeAdapter {
/**
* 源文件宽度
*/
private static int ORI_WIDTH = 300;
/**
* 源文件高度
*/
private static int ORI_HEIGHT = 150;
/**
* 模板图宽度
*/
private static int CUT_WIDTH = 50;
/**
* 模板图高度
*/
private static int CUT_HEIGHT = 50;
/**
* 抠图凸起圆心
*/
private static int circleR = 5;
/**
* 抠图内部矩形填充大小
*/
private static int RECTANGLE_PADDING = 8;
/**
* 抠图的边框宽度
*/
private static int SLIDER_IMG_OUT_PADDING = 1;
// 生成拼图样式
private static int[][] getBlockData(){
int[][] data = new int[CUT_WIDTH][CUT_HEIGHT];
Random random = new Random();
//(x-a)²+(y-b)²=r²
//x中心位置左右5像素随机
double x1 = RECTANGLE_PADDING + (CUT_WIDTH - 2 * RECTANGLE_PADDING) / 2.0 - 5 + random.nextInt(10);
//y 矩形上边界半径-1像素移动
double y1_top = RECTANGLE_PADDING - random.nextInt(3);
double y1_bottom = CUT_HEIGHT - RECTANGLE_PADDING + random.nextInt(3);
double y1 = random.nextInt(2) == 1 ? y1_top : y1_bottom;
double x2_right = CUT_WIDTH - RECTANGLE_PADDING - circleR + random.nextInt(2 * circleR - 4);
double x2_left = RECTANGLE_PADDING + circleR - 2 - random.nextInt(2 * circleR - 4);
double x2 = random.nextInt(2) == 1 ? x2_right : x2_left;
double y2 = RECTANGLE_PADDING + (CUT_HEIGHT - 2 * RECTANGLE_PADDING) / 2.0 - 4 + random.nextInt(10);
double po = Math.pow(circleR, 2);
for (int i = 0; i < CUT_WIDTH; i++) {
for (int j = 0; j < CUT_HEIGHT; j++) {
//矩形区域
boolean fill;
if ((i >= RECTANGLE_PADDING && i < CUT_WIDTH - RECTANGLE_PADDING)
&& (j >= RECTANGLE_PADDING && j < CUT_HEIGHT - RECTANGLE_PADDING)) {
data[i][j] = 1;
fill = true;
} else {
data[i][j] = 0;
fill = false;
}
//凸出区域
double d3 = Math.pow(i - x1, 2) + Math.pow(j - y1, 2);
if (d3 < po) {
data[i][j] = 1;
} else {
if (!fill) {
data[i][j] = 0;
}
}
//凹进区域
double d4 = Math.pow(i - x2, 2) + Math.pow(j - y2, 2);
if (d4 < po) {
data[i][j] = 0;
}
}
}
//边界阴影
for (int i = 0; i < CUT_WIDTH; i++) {
for (int j = 0; j < CUT_HEIGHT; j++) {
//四个正方形边角处理
for (int k = 1; k <= SLIDER_IMG_OUT_PADDING; k++) {
//左上、右上
if (i >= RECTANGLE_PADDING - k && i < RECTANGLE_PADDING
&& ((j >= RECTANGLE_PADDING - k && j < RECTANGLE_PADDING)
|| (j >= CUT_HEIGHT - RECTANGLE_PADDING - k && j < CUT_HEIGHT - RECTANGLE_PADDING +1))) {
data[i][j] = 2;
}
//左下、右下
if (i >= CUT_WIDTH - RECTANGLE_PADDING + k - 1 && i < CUT_WIDTH - RECTANGLE_PADDING + 1) {
for (int n = 1; n <= SLIDER_IMG_OUT_PADDING; n++) {
if (((j >= RECTANGLE_PADDING - n && j < RECTANGLE_PADDING)
|| (j >= CUT_HEIGHT - RECTANGLE_PADDING - n && j <= CUT_HEIGHT - RECTANGLE_PADDING ))) {
data[i][j] = 2;
}
}
}
}
if (data[i][j] == 1 && j - SLIDER_IMG_OUT_PADDING > 0 && data[i][j - SLIDER_IMG_OUT_PADDING] == 0) {
data[i][j - SLIDER_IMG_OUT_PADDING] = 2;
}
if (data[i][j] == 1 && j + SLIDER_IMG_OUT_PADDING > 0 && j + SLIDER_IMG_OUT_PADDING < CUT_HEIGHT && data[i][j + SLIDER_IMG_OUT_PADDING] == 0) {
data[i][j + SLIDER_IMG_OUT_PADDING] = 2;
}
if (data[i][j] == 1 && i - SLIDER_IMG_OUT_PADDING > 0 && data[i - SLIDER_IMG_OUT_PADDING][j] == 0) {
data[i - SLIDER_IMG_OUT_PADDING][j] = 2;
}
if (data[i][j] == 1 && i + SLIDER_IMG_OUT_PADDING > 0 && i + SLIDER_IMG_OUT_PADDING < CUT_WIDTH && data[i + SLIDER_IMG_OUT_PADDING][j] == 0) {
data[i + SLIDER_IMG_OUT_PADDING][j] = 2;
}
}
}
return data;
}
// 抠出拼图
private static void cutImgByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] blockImage, int x, int y) {
for (int i = 0; i < CUT_WIDTH; i++) {
for (int j = 0; j < CUT_HEIGHT; j++) {
int _x = x + i;
int _y = y + j;
int rgbFlg = blockImage[i][j];
int rgb_ori = oriImage.getRGB(_x, _y);
// 原图中对应位置变色处理
if (rgbFlg == 1) {
//抠图上复制对应颜色值
targetImage.setRGB(i,j, rgb_ori);
//原图对应位置颜色变化
oriImage.setRGB(_x, _y, Color.LIGHT_GRAY.getRGB());
} else if (rgbFlg == 2) {
targetImage.setRGB(i, j, Color.WHITE.getRGB());
oriImage.setRGB(_x, _y, Color.GRAY.getRGB());
}else if(rgbFlg == 0){
//int alpha = 0;
targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
}
}
}
}
// 获取图片
private static BufferedImage getBufferedImage(String path) throws IOException{
File file = new File(path);
System.out.println(file.getAbsolutePath());
if(file.isFile()){
return ImageIO.read(file);
}
return null;
}
// 存放图片
private static void writeImg(BufferedImage image, String file) throws Exception {
byte[] imagedata = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(image,"png",bos);
imagedata = bos.toByteArray();
File outFile = new File(file);
System.out.println(outFile.getAbsolutePath());
FileOutputStream out = new FileOutputStream(outFile);
out.write(imagedata);
out.close();
}
该项目作为示意demo,处理完的图片赋随机文件名时仅仅简单加上随机四位后缀。随机取的图片为定高定宽,方便处理。Demo中将这些图片存放到resources文件夹image中,方便随机选取。图片定高定长处理脚本可以看我之前写的 Python实现 jpg图像修改大小并转换为png。
// 处理存放
private static VerificationCodePlace cutAndSave(String imgName, String path, int [][] data, String headPath) throws Exception {
VerificationCodePlace vcPlace =
new VerificationCodePlace("sample_after.png", "sample_after_mark.png", 112, 50);
// 进行图片处理
BufferedImage originImage = getBufferedImage(path);
if(originImage!=null) {
int locationX = CUT_WIDTH + new Random().nextInt(originImage.getWidth() - CUT_WIDTH * 3);
int locationY = CUT_HEIGHT + new Random().nextInt(originImage.getHeight() - CUT_HEIGHT) / 2;
BufferedImage markImage = new BufferedImage(CUT_WIDTH, CUT_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
cutImgByTemplate(originImage, markImage, data, locationX, locationY);
String name = imgName.substring(0, imgName.indexOf('.'));
// 考虑图片覆盖,简单设置四位随机数
int r = (int)Math.round(Math.random() * 8999) + 1000;
String afterName = name + "_after" + r + ".png";
String markName = name + "_after_mark" + r + ".png";
writeImg(originImage, headPath + afterName);
writeImg(markImage, headPath + markName);
vcPlace = new VerificationCodePlace(afterName, markName, locationX, locationY);
}
return vcPlace;
}
// 获取文件夹下所有文件名
private static ArrayList<String> getFileNamesFromDic(String dicPath){
File dic = new File(dicPath);
ArrayList<String> imageFileNames = new ArrayList<String>();
File[] dicFileList = dic.listFiles();
for(File f: dicFileList){
imageFileNames.add(f.getName());
}
return imageFileNames;
}
// 总流程,随机获取图片并处理,将拼图和对应图片存放至after_img
// 出错则返回sample
// headPath为存放生成图片的文件夹地址
public static VerificationCodePlace getRandomVerificationCodePlace(String headPath) {
VerificationCodePlace vcPlace = new VerificationCodePlace("sample_after.png", "sample_mark_after.png", 112, 50);
// 从文件夹中读取所有待选择文件
String directoryPath = "src/main/resources/static/image";
ArrayList<String> imageFileNames = getFileNamesFromDic(directoryPath);
// 随机获取
int r = (int)Math.round(Math.random() * (imageFileNames.size() - 1));
String imgName = imageFileNames.get(r);
String path = "src/main/resources/static/image/" + imgName;
int[][] data = VerificationCodeAdapter.getBlockData();
// 进行图片处理
try {
vcPlace = cutAndSave(imgName, path, data, headPath);
} catch (Exception e) {
e.printStackTrace();
return vcPlace;
}
return vcPlace;
}
参数headpath为处理后文件存放处。为了防止出错时前端没有收到返回,在文件夹中默认放置两张sample图片,在后端不能成功切割时进行数据的返回。
前端计划使用ajax进行数据的获取,首先写一个简单get来处理图片返回信息,同时一个页面用来显示验证码。
@RequestMapping("/getImgInfo")
@ResponseBody
// 随机获取背景和拼图,返回json
public String imgInfo(){
VerificationCodePlace vcPlace =VerificationCodeAdapter.getRandomVerificationCodePlace(imgLocation);
ObjectMapper om = new ObjectMapper();
String jsonResult = "";
try {
jsonResult = om.writeValueAsString(vcPlace);
return jsonResult;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return jsonResult;
}
@RequestMapping("/vcode")
// 验证码主界面
public String vCode(){
return "vc_sample.html";
}
由于Springboot是自带tomcat,服务器文件夹为临时文件夹,不能直接设置获取图片文件。故本demo将图片使用额外的文件夹存放。为了保证前端能通过url获取到对应的图片信息,增加config类。
@Configuration
public class ResourceConfig implements WebMvcConfigurer {
// 为了前端能访问生成图片,进行本地映射
@Value("${afterImage.resourceHandler}")
private String resourceHandler;
@Value("${afterImage.location}")
private String location;
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler(resourceHandler).addResourceLocations("file:///" + location);
}
}
config类中设置对应的url前缀映射的文件地址。application.yml
中加入配置信息。
afterImage:
resourceHandler: /after/**
location: D:/vc_image/
考虑到正式使用时图片是会存放在服务器中,需要定期进行删除来减轻服务器的负担,给出了删除图片的方法。
Adapter类中:
// 删除after中的图片文件
public static String deleteAfterImage(String headPath){
boolean successDelete = true;
int sum = 0;
float fileSize = 0;
String directoryPath = headPath;
File dic = new File(directoryPath);
File[] dicFileList = dic.listFiles();
if(dicFileList != null) {
for (File f : dicFileList) {
if (!f.getName().equals("sample_after.png") && !f.getName().equals("sample_after_mark.png")) {
long fLength = f.length();
successDelete = f.delete();
if(!successDelete)
break;
sum ++;
fileSize += fLength;
}
}
}
float fileSizeInMB = fileSize / 1024 / 1024;
if(!successDelete){
String tip = "拼图文件删除中出现错误,请到" + directoryPath + "中进行查看";
System.out.println(tip);
return tip;
}else{
String tip = "拼图文件删除成功,删除文件数量为" + sum + ",文件总大小为" + fileSizeInMB + "MB";
System.out.println(tip);
return tip;
}
}
controller中:
@RequestMapping("/deleteImg")
@ResponseBody
// 删除生成的验证码图片
public String deleteImg(){
return VerificationCodeAdapter.deleteAfterImage(imgLocation);
}
返回数据:
{“backName”:“vc1_min_after9841.png”,“markName”:“vc1_min_after_mark9841.png”,“xLocation”:74,“yLocation”:89}
前端使用即可。
下篇文章讲下前端实现。
项目开源地址:
https://github.com/huiluczP/verification_code_demo