实现
graph LR
A[前端发起验证码图片请求] --> B[后端接收到请求,对图片进行处理,生成一个随机token]
C[后端将处理后的图片坐标和token存入redis,再将处理后的图片和token返回给前端] -->
D[前端接收到用户处理用户滑动验证码后的坐标,返回给后端] --> E[后端从redis中读取出坐标,和前端回传的坐标进行比较,返回相应结果]
代码实现
图片处理工具类
package com.example.demo.util;
import org.springframework.stereotype.Component;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@Component
public class VerifyImageUtil {
/**
* 源文件宽度
*/
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;
/**
* 根据传入的路径生成指定验证码图片
*
* @param filePath
* @return
* @throws IOException
*/
public VerifyImage getVerifyImage(String filePath) throws IOException {
BufferedImage srcImage = ImageIO.read(new File(filePath));
int locationX = CUT_WIDTH + new Random().nextInt(srcImage.getWidth() - CUT_WIDTH * 3);
int locationY = CUT_HEIGHT + new Random().nextInt(srcImage.getHeight() - CUT_HEIGHT) / 2;
BufferedImage markImage = new BufferedImage(CUT_WIDTH,CUT_HEIGHT,BufferedImage.TYPE_4BYTE_ABGR);
int[][] data = getBlockData();
cutImgByTemplate(srcImage, markImage, data, locationX, locationY);
return new VerifyImage(getImageBASE64(srcImage),getImageBASE64(markImage),locationX,locationY);
}
/**
* 生成随机滑块形状
* <p>
* 0 透明像素
* 1 滑块像素
* 2 阴影像素
* @return int[][]
*/
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;
}
/**
* 裁剪区块
* 根据生成的滑块形状,对原图和裁剪块进行变色处理
* @param oriImage 原图
* @param targetImage 裁剪图
* @param blockImage 滑块
* @param x 裁剪点x
* @param y 裁剪点y
*/
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);
}
}
}
}
/**
* 随机获取一张图片对象
* @param path
* @return
* @throws IOException
*/
public static BufferedImage getRandomImage(String path) throws IOException {
File files = new File(path);
File[] fileList = files.listFiles();
List<String> fileNameList = new ArrayList<>();
if (fileList!=null && fileList.length!=0){
for (File tempFile:fileList){
if (tempFile.isFile() && tempFile.getName().endsWith(".jpg")){
fileNameList.add(tempFile.getAbsolutePath().trim());
}
}
}
Random random = new Random();
File imageFile = new File(fileNameList.get(random.nextInt(fileNameList.size())));
return ImageIO.read(imageFile);
}
/**
* 将IMG输出为文件
* @param image
* @param file
* @throws Exception
*/
public static void writeImg(BufferedImage image, String file) throws Exception {
byte[] imagedata = null;
ByteArrayOutputStream bao=new ByteArrayOutputStream();
ImageIO.write(image,"png",bao);
imagedata = bao.toByteArray();
FileOutputStream out = new FileOutputStream(new File(file));
out.write(imagedata);
out.close();
}
/**
* 将图片转换为BASE64
* @param image
* @return
* @throws IOException
*/
public static String getImageBASE64(BufferedImage image) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image,"png",out);
//转成byte数组
byte[] bytes = out.toByteArray();
BASE64Encoder encoder = new BASE64Encoder();
//生成BASE64编码
return encoder.encode(bytes);
}
/**
* 将BASE64字符串转换为图片
* @param base64String
* @return
*/
public static BufferedImage base64StringToImage(String base64String) {
try {
BASE64Decoder decoder=new BASE64Decoder();
byte[] bytes1 = decoder.decodeBuffer(base64String);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes1);
return ImageIO.read(bais);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
图片属性类
package com.example.demo.util;
import lombok.Data;
@Data
public class VerifyImage{
// 原图
String srcImage;
// 抠图后的图
String cutImage;
// 滑块坐标点
Integer XPosition;
Integer YPosition;
public VerifyImage(String srcImage, String cutImage, Integer XPosition, Integer YPosition) {
this.srcImage = srcImage;
this.cutImage = cutImage;
this.XPosition = XPosition;
this.YPosition = YPosition;
}
}
redis处理的业务类
package com.example.demo.service;
import com.example.demo.util.VerifyImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedisService {
@Autowired
RedisTemplate redisTemplate;
public void set(String key, Object value) {
//更改在redis里面查看key编码问题
RedisSerializer redisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
ValueOperations<String, Object> vo = redisTemplate.opsForValue();
vo.set(key, value);
}
// 为指定的key和value指定过期时间
public void setex(String key, Object value, int seconds) {
ValueOperations<String, Object> vo = redisTemplate.opsForValue();
vo.set(key, value, seconds, TimeUnit.SECONDS);
}
public Object get(String key) {
ValueOperations<String, Object> vo = redisTemplate.opsForValue();
return vo.get(key);
}
public boolean delete(String key) {
return redisTemplate.delete(key);
}
}
图片处理和图片验证的业务类
package com.example.demo.service;
import com.example.demo.util.VerifyImage;
import com.example.demo.util.VerifyImageUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.logging.Logger;
/**
* @author lyd
* @Description: 一个接口返回两个图片
* @date 2021/6/1718:13
*/
@Service
public class VerifyServiceOneInterface {
Logger logger = Logger.getLogger("日志");
@Autowired
VerifyImageUtil verifyImageUtil;
@Autowired
RedisService redisService;
// 定义一个Ant模式通配符的Resource查找器,可以用来查找类路径下或者文件系统中的资源。
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
public Object getImage() throws IOException {
// 生成token
String token = UUID.randomUUID().toString();
redisService.setex("AUTH:RAND:" + token, token, 60);
logger.info("token:" + token);
// 得到图片文件夹中所有路径
Resource[] resources = resolver.getResources("classpath*:/verifyImg/*");
int ranNum = new Random().nextInt(resources.length);
// 通过随机的图片路径得到验证码图片
VerifyImage verifyImage = verifyImageUtil.getVerifyImage(String.valueOf(resources[ranNum].getFile()));
// 将裁剪后的图片坐标存入redis中
redisService.set("getXPosition", verifyImage.getXPosition());
redisService.set("getYPosition", verifyImage.getYPosition());
logger.info("ImageX:" + verifyImage.getXPosition());
logger.info("ImageY:" + verifyImage.getYPosition());
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("token", token);
// 去除base64中的特殊字符
String strSrcImage = verifyImage.getSrcImage().replaceAll("[\s*
]", "");
String strCutImage = verifyImage.getCutImage().replaceAll("[\s*
]", "");
resultMap.put("srcImage", strSrcImage);
resultMap.put("cutImage", strCutImage);
return resultMap;
}
/**
* 得到图片验证的坐标,进行有效性校对
*
* @return
*/
public String verifyImage(String x, String y, HttpServletRequest httpServletRequest) {
// 得到客户端token
String token = httpServletRequest.getHeader("token");
// 得到redis中token
String redisToken = (String) redisService.get("AUTH:RAND:" + token);
if (redisToken == null) {
return "验证码过期";
}
if(StringUtils.isEmpty(x) ||StringUtils.isEmpty(y) ){
return "验证码为空";
}
Integer ImageX = (Integer) redisService.get("getXPosition");
Integer ImageY = (Integer) redisService.get("getYPosition");
// 计算验证图片坐标的误差值
int absX = Math.abs(Integer.parseInt(x) - ImageX);
int absY = Math.abs(Integer.parseInt(y) - ImageY);
if (absX < 6 && absY < 6) {
return "验证码正确";
}
return "验证码错误";
}
}
实现一个controller类,负责和前端页面进行交互
package com.example.demo.controller;
import com.example.demo.service.VerifyService;
import com.example.demo.service.VerifyServiceOneInterface;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author lyd
* @Description:
* @date 2021/6/1718:16
*/
@RestController
@RequestMapping("VerifyOneInterfaceController")
public class VerifyOneInterfaceController {
@Autowired
VerifyServiceOneInterface verifyServiceOneInterface;
@RequestMapping("getImage")
@ResponseBody
public Object getImage() throws IOException {
return verifyServiceOneInterface.getImage();
}
@RequestMapping("verifyImage")
@ResponseBody
public String verifyImage(String x, String y, HttpServletRequest httpServletRequest) {
return verifyServiceOneInterface.verifyImage(x, y, httpServletRequest);
}
}
这里如果没有前端页面,使用postman或者浏览器进行测试的时候,记得要给heard中添加一个token值,token是在后台生成的