前提
图片验证码的工具是两个版本 简单的图片验证码是网上翻来的
业务逻辑很简单,生成一个图片验证码,保存到Session里,然后将图片输入注入Response对象 传递给前端,显示出来
前端用户把验证码图片中的数字提交到后端
后端从Session把验证码拿到 跟用户输入的作比对 如此就完成了验证码的逻辑
复杂版的是纯原创,基于Redis的缓存功能 将工具生成的图片正码数字保存到Redis,同时利用MyBatisPlus提供的雪花算法生成一个Redis的Key值,将key值和验证码图片一起发送给前端,前端再用户输入验证码后,将验证码和对应的Key值一起传回后端
后端通过Key值去Redis拿到验证码的号码,于用户输入的数据做比对 如此也能完成验证码的逻辑
这里有两个小细节
Session模式是直出图片给前端 从前端角度来讲 接收到的就是一个普通图片的 直接显示就完事儿了
Redis模式是返回一个Json数据 里边包含了Base64之后的图片数据 需要前端进行一定的处理才能显示
另外
Redis版本的图片 还用到了JPEGEncode的图片压缩 貌似这种尺度的压缩不是很明显 这个后续再研究
Session版 简单高效
- Util 工具类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/***
* Author: YL.Lou
* Class: RandomValidateCodeUtil
* Project: base
* Introduce:
* DateTime: 2022-06-30 13:59
***/
@Slf4j
@Component
public class VerifyCodeImageUtil {
@Autowired
private Result result;
public static final String RANDOM_CODE_KEY= "RANDOM_VALIDATE_CODE_KEY";//放到session中的key
private int width = 95;// 图片宽
private int height = 25;// 图片高
private int stringCount = 4;// 随机产生字符数量
private int fontSize = 18; // 字符大小
private Random random = new Random();
/**
* 获得字体 HANGING_BASELINE CENTER_BASELINE PLAIN
*/
private Font getFont() {
return new Font("Fixedsys", Font.BOLD, fontSize);
}
/**
* 获得颜色
*/
private Color getRandColor(int fc, int bc) {
int r = BaseUtil.getRandomNum(fc,bc);
int g = BaseUtil.getRandomNum(fc,bc);
int b = BaseUtil.getRandomNum(fc,bc);
return new Color(r, g, b);
}
/**
* 生成随机图片
*/
public void getRandCode(
Integer w,
Integer h,
Integer l,
Integer s,
HttpServletRequest request,
HttpServletResponse response
) {
HttpSession session = request.getSession();
if (null != w) this.width = w;
if (null != h) this.height = h;
if (null != l) this.stringCount = l;
if (null != s) this.fontSize = s;
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics g = image.getGraphics();// 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
g.fillRect(0, 0, width, height);//图片大小
// 绘制背景干扰线
for (int i = 0; i <= 10; i++) {
drawLine(g);
}
// 绘制随机字符
String randomString = BaseUtil.getRandom(stringCount);
for (int i = 0; i < stringCount; i++) {
drawString(g, randomString, i);
}
// 绘制前景干扰线
for (int i = 0; i <= 5; i++) {
drawLine(g);
}
//将生成的随机字符串保存到session中
session.removeAttribute(RANDOM_CODE_KEY);
session.setAttribute(RANDOM_CODE_KEY, randomString);
// 释放资源
g.dispose();
try {
// 将内存中的图片通过流动形式输出到客户端
ImageIO.write(image, "JPEG", response.getOutputStream());
} catch (Exception e) {
log.error("将图片流输出到客户端失败", e);
}
}
/**
* 绘制字符串
*/
private void drawString(Graphics g, String randomString, int i) {
g.setFont(getFont());
g.setColor(getRandColor(33, 99));
String str = getRandomString(randomString,i);
int x = fontSize * i + BaseUtil.getRandomNum(1, 3);
int y = fontSize + BaseUtil.getRandomNum(-2, 2) ;
g.drawString(str,x,y);
}
/**
* 绘制干扰线
*/
private void drawLine(Graphics g) {
g.setColor(getRandColor(150,250));
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(width / 2);
int yl = random.nextInt(height / 2);
g.drawLine(x, y, x + xl, y + yl);
}
/**
* 获取随机的字符
*/
public String getRandomString(String randomString, int num) {
char[] chars = randomString.toCharArray();
return String.valueOf(chars[num]);
}
/**
* 校验图片验证码
*/
public boolean checkCode(HttpServletRequest request, String code){
HttpSession session = request.getSession();
if (null == session){
return false;
}
Object attribute = session.getAttribute(RANDOM_CODE_KEY);
if (null == attribute || null == code){
return false;
}
String sessionCode = attribute.toString();
if (null == sessionCode || !code.equals(sessionCode)){
return false;
}
return true;
}
}
- Controller 层 输出验证码
/**
* 生成并返回验证码图片
*/
@GetMapping(value = "/imgCode")
public void getImgCode(
@Min(value = 48, message = "验证码图片宽度不能低于 48")@RequestParam(value = "width", required = false, defaultValue = "95") Integer width,
@Min(value = 16, message = "验证码图片高度不能低于 16")@RequestParam(value = "height", required = false, defaultValue = "25") Integer height,
@Min(value = 4, message = "验证码字符长度不能低于 4")@RequestParam(value = "length",required = false, defaultValue = "4") Integer length,
@Min(value = 12, message = "验证码字符大小不能低于 12") @RequestParam(value = "size",required = false, defaultValue = "18") Integer size,
HttpServletRequest request,
HttpServletResponse response) {
try {
response.setContentType("image/jpeg");//设置相应类型,告诉浏览器输出的内容为图片
response.setHeader("Pragma", "No-cache");//设置响应头信息,告诉浏览器不要缓存此内容
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expire", 0);
verifyCodeImageUtil.getRandCode(width, height, length, size, request, response);//输出验证码图片方法
} catch (Exception e) {
log.error("获取验证码失败", e);
}
}
- Controller 层 核对验证码
/**
* 获取手机验证码
* @param uPhone 手机号
* @param imgCode 图片验证码
* @param request request
* @return
*/
@PostMapping(value = "/smsCode")
public Result getSmsCode(
@NotBlank(message = "手机号码不能为空") @Pattern(regexp = "^\\d{11}$", message = "手机号码应为11位数字") @RequestParam("uPhone") String uPhone,
@NotBlank(message = "图片验证码不能为空") @Pattern(regexp = "^\\d{4}$", message = "图片验证码应为4位数字") @RequestParam("imgCode") String imgCode,
HttpServletRequest request
){
// 验证图片验证码是否正确
if (!verifyCodeImageUtil.checkCode(request, imgCode)){
return result.failed("图片验证码输入错误");
}
//调起service 获得短信发送的最终状态
return publicService.getSmsCode(uPhone);
}
Redis版本 支持集群部署
- Util 工具类
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/***
* Author: YL.Lou
* Class: RandomValidateCodeUtil
* Project: base
* Introduce: 生成一个随机字符串图片 利用Redis进行图片验证
***/
@Slf4j
@Component
public class VerifyCodeImageUtil {
// 包装返回结果用的
@Autowired
private Result result;
// 存储Redis数据用的
@Autowired
private RedisUtil redisUtil;
// 定义图片的默认大小
private int width = 95;// 图片宽
private int height = 25;// 图片高
private int stringCount = 4;// 随机产生字符数量
private int fontSize = 18; // 字符大小
/**
* 获得字体 HANGING_BASELINE CENTER_BASELINE PLAIN
*/
private Font getFont() {
return new Font("Fixedsys", Font.BOLD, fontSize);
}
/**
* 获得颜色
*/
private Color getRandColor(int fc, int bc) {
int r = BaseUtil.getRandomNum(fc,bc);
int g = BaseUtil.getRandomNum(fc,bc);
int b = BaseUtil.getRandomNum(fc,bc);
return new Color(r, g, b);
}
/**
* 生成随机图片
*/
public Result getRandCode(
Integer w,
Integer h,
Integer l,
Integer s
) {
if (null != w) this.width = w;
if (null != h) this.height = h;
if (null != l) this.stringCount = l;
if (null != s) this.fontSize = s;
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics g = image.getGraphics();// 产生Image对象的Graphics对象,改对象可以在图像上进行各种绘制操作
g.fillRect(0, 0, width, height);//图片大小
// 绘制背景干扰线
for (int i = 0; i <= 5; i++) {
drawLine(g);
}
// 绘制随机字符
String randomString = BaseUtil.getRandom(stringCount);
for (int i = 0; i < stringCount; i++) {
drawString(g, randomString, i);
}
// 绘制前景干扰线
for (int i = 0; i <= 5; i++) {
drawLine(g);
}
// 释放画笔资源
g.dispose();
// 保存缓存值
String key = String.valueOf(IdWorker.getId());
// 保存code到Redis 60分钟过期
redisUtil.add(RedisPrefix.IMG_CODE + key, randomString, 30, TimeUnit.MINUTES);
// 图片数据的容器
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 将数据写入容器
try {
ImageIO.write(image, "JPEG", byteArrayOutputStream);
} catch (IOException e) {
return result.failed("生成验证码图片失败");
}
// 将容器中的数据进行JPEG压缩
JPEGImageEncoder jpegEncoder = JPEGCodec.createJPEGEncoder(byteArrayOutputStream);
JPEGEncodeParam jpegEncodeParam = jpegEncoder.getDefaultJPEGEncodeParam(image);
jpegEncodeParam.setQuality(0.75f, true);
jpegEncoder.setJPEGEncodeParam(jpegEncodeParam);
try {
jpegEncoder.encode(image, jpegEncodeParam);
} catch (IOException e) {
return result.failed("图片压缩失败");
}
// 释放图片资源
image.flush();
// 得到JPEG数据
Base64.Encoder encoder = Base64.getEncoder();
String data = encoder.encodeToString(byteArrayOutputStream.toByteArray());
// 将Redis的Key值 和 Base64数据打包
HashMap<String, String> stringStringHashMap = new HashMap<>();
stringStringHashMap.put("imgKey",key);
stringStringHashMap.put("imgData","data:image/jpeg;base64," + data);
// 将打包好的数据返回给Controller
return result.success("生成验证码图片成功", stringStringHashMap);
}
/**
* 绘制字符串
*/
private void drawString(Graphics g, String randomString, int i) {
g.setFont(getFont());
g.setColor(getRandColor(30, 90));
String str = getRandomString(randomString,i);
int x = fontSize * i + BaseUtil.getRandomNum(1, 3);
int y = fontSize + BaseUtil.getRandomNum(-2, 2) ;
g.drawString(str,x,y);
}
/**
* 绘制干扰线
*/
private void drawLine(Graphics g) {
g.setColor(getRandColor(100,200));
int x = BaseUtil.getRandomNum(5, width - 5);
int y = BaseUtil.getRandomNum(5, height - 5);
int xl = BaseUtil.getRandomNum(5, width - 5);
int yl = BaseUtil.getRandomNum(5, height - 5);
g.drawLine(x, y, xl, yl);
}
/**
* 获取随机的字符
*/
public String getRandomString(String randomString, int num) {
char[] chars = randomString.toCharArray();
return String.valueOf(chars[num]);
}
/**
* 核对图片验证码输入是否正确(Redis isMulti = true)
* @param code 用户输入的验证码
* @param key 前端传回的 用来去Redis拉取数据的 Key值
* @return 是否核对成功 true 成功 false 失败
*/
public boolean checkImgCode(String code, String key) {
// 去Redis 拿到 Code
String originCode = redisUtil.get(RedisPrefix.IMG_CODE + key);
// 判断是否拿到codeFromRedis
if (null == originCode){
return false;
}
// 判断code是否匹配
if (!code.equals(originCode)){
return false;
}
// 将Redis删除 留着也没用
redisUtil.delete(RedisPrefix.IMG_CODE + key);
// 最后返回核验成功
return true;
}
}
- Controller 层 输出验证码
/**
* 生成并返回验证码图片Base64方式返回
*/
@GetMapping(value = "/imgCode")
public Result getImgCode(
@Min(value = 48, message = "验证码图片宽度不能低于 48")@RequestParam(value = "width", required = false, defaultValue = "95") Integer width,
@Min(value = 16, message = "验证码图片高度不能低于 16")@RequestParam(value = "height", required = false, defaultValue = "25") Integer height,
@Min(value = 4, message = "验证码字符长度不能低于 4")@RequestParam(value = "length",required = false, defaultValue = "4") Integer length,
@Min(value = 12, message = "验证码字符大小不能低于 12") @RequestParam(value = "size",required = false, defaultValue = "18") Integer size
) {
// 返回二维码图片的Base64数据
return verifyCodeImageUtil.getRandCode(width, height, length, size);
}
- Controller 层 核对验证码
/**
* 获取手机验证码
* @param uPhone 手机号
* @param imgCode 图片验证码
* @param imgKey 验证码图片Key值
* @return
*/
@PostMapping(value = "/smsCode")
public Result getSmsCode(
@NotBlank(message = "手机号码不能为空") @Pattern(regexp = "^\\d{11}$", message = "手机号码应为11位数字") @RequestParam("uPhone") String uPhone,
@NotBlank(message = "图片验证码不能为空") @Pattern(regexp = "^\\d{4}$", message = "图片验证码应为4位数字") @RequestParam("imgCode") String imgCode,
@NotBlank(message = "前端未传回Key值") @RequestParam("imgKey") String imgKey
){
// 验证图片验证码是否正确
if (verifyCodeImageUtil.checkImgCode(imgCode, imgKey)){
return result.failed("图片验证码输入错误");
}
//调起service 获得短信发送的最终状态
return publicService.getSmsCode(uPhone);
}