概要介绍
在一些项目的场景下,可能需要我们来实现一些如下样式的验证码效果
第一种自写工具类
使用工具类生成图片验证码,无需引入什么依赖,修改性更强,样式如上图
第一种
1.编写一个CaptchaUtil.java的工具类
package com.vip.wmlcsys.util;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* @Author Chenry.r
* @Date 13/10/2023 下午3:17
* @Version 1.0
* @Description <p>备注:captcha验证码</p>
*/
public class CaptchaUtil {
public static String drawRandomText(int width, int height, BufferedImage verifyImg) {
Graphics2D graphics = (Graphics2D) verifyImg.getGraphics();
graphics.setColor(Color.WHITE);//设置画笔颜色-验证码背景色
graphics.fillRect(0, 0, width, height);//填充背景
graphics.setFont(new Font("微软雅黑", Font.BOLD, 40));
//数字和字母的组合
String baseNumLetter = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
StringBuffer sBuffer = new StringBuffer();
int x = 10; //旋转原点的 x 坐标
String ch = "";
Random random = new Random();
for (int i = 0; i < 4; i++) {
graphics.setColor(getRandomColor());
//设置字体旋转角度
int degree = random.nextInt() % 30; //角度小于30度
int dot = random.nextInt(baseNumLetter.length());
ch = baseNumLetter.charAt(dot) + "";
sBuffer.append(ch);
//正向旋转
graphics.rotate(degree * Math.PI / 180, x, 45);
graphics.drawString(ch, x, 45);
//反向旋转
graphics.rotate(-degree * Math.PI / 180, x, 45);
x += 48;
}
//画干扰线
for (int i = 0; i < 6; i++) {
// 设置随机颜色
graphics.setColor(getRandomColor());
// 随机画线
graphics.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height));
}
//添加噪点
for (int i = 0; i < 30; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
graphics.setColor(getRandomColor());
graphics.fillRect(x1, y1, 2, 2);
}
return sBuffer.toString();
}
//随机取色
private static Color getRandomColor() {
Random ran = new Random();
Color color = new Color(ran.nextInt(256), ran.nextInt(256), ran.nextInt(256));
return color;
}
}
引用CaptchaUtil 工具类生成图片验证码返回图片
/*
* @Des: 获取验证码图片
*
* @Author: Chenry.r
* @Date: 23/10/2023 下午2:05
* @param response:
* @param request:
*/
@ApiOperation(value = "获取验证码功能",produces = "image/png")
@GetMapping ("/captcha")
public void getCaptchaCode(HttpServletResponse response, HttpServletRequest request) {
try {
int width = 200;
int height = 69;
BufferedImage verifyImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
/**
* 1、生成对应宽高的初始图片
* 2、单独的一个类方法,出于代码复用考虑,进行了封装。
* 3、功能是生成验证码字符并加上噪点,干扰线,返回值为验证码字符
*/
String randomText = CaptchaUtil.drawRandomText(width, height, verifyImg);
System.out.println(randomText);
// 验证码放入redis缓存里,用于后端验证验证码。
redisUtil.set("captcha",randomText,60);
// .......这里存在一个问题留在后面解决(当多个用户同一时间点登录时redis里只有一个最新的验证码)
// 我这里是放在redis,登录时从redis里获取比较
/*request.getSession().removeAttribute("verifyCode");
request.getSession().setAttribute("verifyCode", randomText);*/
response.setContentType("image/png");//必须设置响应内容类型为图片,否则前台不识别
OutputStream os = response.getOutputStream(); //获取文件输出流
ImageIO.write(verifyImg, "png", os);//输出图片流
os.flush();
os.close();//关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
在前端的展示与刷新、前端我这里是使用layuimini
<div id="validatePanel" class="item" style="width: 137px;">
<input type="text" name="captcha" id="captcha" lay-verify="required" placeholder="请输入验证码"
maxlength="4">
<img id="refreshCaptcha" class="validateImg" th:src="@{/login/captcha}">
</div>
点击图片刷新验证码
//点击验证码进行切换
$("#refreshCaptcha").click(function () {
$("#captcha").val('');// 清除name="captcha"的输入框的值
// 给 img对象的 src属性 ,/login/captcha, 加一个时间戳防止缓存
$(this).attr("src", getProjectUrl() + "/login/captcha?" + Math.random());
});
登录时验证码的比较
@ApiOperation(value = "登录用户验证")
@PostMapping("doindex")
public RespBean doindex(UserLoginParam userLoginParam) {
String captcha = (String) redisUtil.get("captcha");
if (StringUtils.isEmpty(captcha)){
return RespBean.error("验证码过期重新刷新在试一吧");
}
// equalsIgnoreCase() 不区分大小写
if (!captcha.equalsIgnoreCase(userLoginParam.getCaptcha())){
return RespBean.error("验证码错误");
}
// ***********其它的比较和其它的登录流程***********************
// ........
return RespBean.success("登录成功");
}
最后:仔细想一下这样处理的验证码放到redis里会有一个问题就是:
当多个用户在同一时间点登录时,稍微慢几秒的那个用户的验证码就会给之前哪些还没有登录成功的验证码个覆盖到,所以想到这里我改为了一下的验证方式获取验证码,同时也使用一下Kaptcha;其实上面的方式也是可以的.
思路:只需要把图片转为Base64字符串+uuid(uuid也就是定义一个redis的值返回给前端)同时返回给前端,验证时根据uuid去redis里查询
第二种使用Kaptcha
引入Kaptcha需要的maven依赖
<!--验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
创建一个Kaptcha的配置文件CaptchaConfig.java
package com.vip.wmlcsys.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
import static com.google.code.kaptcha.Constants.*;
/**
* @Author Chenry.r
* @Date 9/12/2023 下午11:51
* @Version 1.0
* @Description <p>备注:Kaptcha验证码配置类CaptchaConfig </p>
*/
@Configuration
public class CaptchaConfig {
/*
* @Des: 文字验证码
*
* @Author: Chenry.r
* @Date: 10/12/2023 下午9:02
* @return com.google.code.kaptcha.impl.DefaultKaptcha
*/
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
/*
* @Des: 算数法验证码
*
* @Author: Chenry.r
* @Date: 10/12/2023 下午9:02
* @return com.google.code.kaptcha.impl.DefaultKaptcha
*/
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "200");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "69");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "40");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.vip.wmlcsys.config.KaptchaTextCreator");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
验证码文本生成器(算术验证码的时候需要) 算术验证码配置规则KaptchaTextCreator
package com.vip.wmlcsys.config;
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
import java.util.Random;
/**
* @Author Chenry.r
* @Date 9/12/2023 下午11:56
* @Version 1.0
* @Description <p>备注:算术验证码配置规则 KaptchaTextCreator</p>
*/
public class KaptchaTextCreator extends DefaultTextCreator {
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
@Override
public String getText() {
Integer result = 0;
// 生成0-10随机数
Random random = new Random();
int x = random.nextInt(10);
int y = random.nextInt(10);
// StringBuilder 用于字符串拼接,但效率更高
StringBuilder suChinese = new StringBuilder();
// 生成0-2随机数,用来生成加减乘除
int randomoperands = (int) Math.round(Math.random() * 2);
if (randomoperands == 0) {
result = x * y;
suChinese.append(CNUMBERS[x]);
suChinese.append("*");
suChinese.append(CNUMBERS[y]);
} else if (randomoperands == 1) {
if (!(x == 0) && y % x == 0) {
result = y / x;
suChinese.append(CNUMBERS[y]);
suChinese.append("/");
suChinese.append(CNUMBERS[x]);
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
} else if (randomoperands == 2) {
if (x >= y) {
result = x - y;
suChinese.append(CNUMBERS[x]);
suChinese.append("-");
suChinese.append(CNUMBERS[y]);
} else {
result = y - x;
suChinese.append(CNUMBERS[y]);
suChinese.append("-");
suChinese.append(CNUMBERS[x]);
}
} else {
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
suChinese.append("=?@" + result);
return suChinese.toString();
}
}
获取验证码的请求接口(KaptcheController)
package com.vip.wmlcsys.controller;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FastByteArrayOutputStream;
import com.google.code.kaptcha.Producer;
import com.vip.wmlcbasic.util.RedisUtil;
import com.vip.wmlcbasic.vo.RespBean;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.UUID;
/**
* @Author Chenry.r
* @Date 9/12/2023 下午11:58
* @Version 1.0
* @Description <p>备注:获取验证码 KaptcheController </p>
*/
@Api(tags = "获取验证码的请求接口(KaptcheController)")
@RestController
@RequestMapping("/captImg")
public class KaptcheController {
// 文字验证码
@Resource(name = "captchaProducer")
private Producer captchaProducer;
// 算术验证码
@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;
// 验证码类型
@Value("${kaptche.captchaType}")
private String captchaType;
@Autowired
private RedisUtil redisUtil;
/*
* @Des: 直接返回图片给前端,这次不是使用这个方式
*
* @Author: Chenry.r
* @Date: 10/12/2023 下午8:59
* @param response:
* @param session:
*/
@RequestMapping("/getCaptImg")
public void getCaptImg(HttpServletResponse response, HttpSession session) {
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
if ("math".equals(captchaType)) {
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
} else if ("char".equals(captchaType)) {
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
try {
response.setContentType("image/png");
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
System.out.println("响应验证码失败" + e.getMessage());
}
}
/*
* @Des: 返回base64字符串,前端进行解析
*
* @Author: Chenry.r
* @Date: 10/12/2023 下午3:29
* @param euuid: 前端传过来的上一个验证码的key值
* @return com.vip.wmlcbasic.vo.RespBean
*/
@RequestMapping("/captcha{euuid}")
public RespBean getCapt(@PathVariable String euuid) {
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
if ("math".equals(captchaType)) {
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
} else if ("char".equals(captchaType)) {
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", os);
} catch (IOException e) {
return RespBean.error(e.getMessage());
}
//生成uuid用于表示多用户同时验证码只记录1问题
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
HashMap<String, String> map = new HashMap<>();
map.put("uuid", uuid);
map.put("img", Base64.encode(os.toByteArray()));
redisUtil.set("captcha-" + uuid, code, 60);
if (!euuid.equals("undefined")) {
String captcha = (String) redisUtil.get("captcha-" + euuid);
if (!StringUtils.isEmpty(captcha)) {
redisUtil.del("captcha-" + euuid);
}
}
return RespBean.success("验证码加载成功", map);
}
}
验证码类型选着application-dev.yml
kaptche:
captchaType : char # 1、math表示验证码为计算结果,2、char表示为字母数字
前端的使用方式
<div id="validatePanel" class="item" style="width: 137px;">
<input type="text" name="captcha" id="captcha" lay-verify="required" placeholder="请输入验证码"
maxlength="4">
<img id="refreshCaptcha" class="validateImg">
</div>
<input type="text" name="uuid" id="uuid" lay-verify="required" hidden autocomplete="off"/>
// 初始化加载验证码
init();
// 定义一个euuid 用于返回它的上一个验证码的uuid,用于防止多用户直至在刷新验证码造成redis无用的key值堆积
var euuid;
// 加载验证码
function init() {
$.ajax({
type: "get",
url: getProjectUrl() + "/captImg/captcha"+euuid,
dataType: 'JSON',
success: function (msg) {
if (msg.code == '200' && msg.message == "验证码加载成功") {
var data = msg.obj
// 选取要赋值的元素
var imgElement = document.getElementById('refreshCaptcha');
// 将值赋值给src属性
imgElement.src = "data:image/gif;base64," + data.img;
euuid = data.uuid;
// 选取要赋值的元素
var uuidElement = document.getElementById('uuid');
// 将值赋值给src属性
uuidElement.value = data.uuid;
}
}
});
}
//点击验证码进行切换
$("#refreshCaptcha").click(function () {
// 清除name="captcha"的输入框的值
$("#captcha").val('');
init();
});
最后 登录时的验证
/*
* @Des: 登录流程操作过程
*
* @Author: Chenry.r
* @Date: 20/10/2023 下午3:57
* @param userLoginParam: 用户登录时输入信息
* @return com.vip.wmlcbasic.vo.RespBean
*/
@ApiOperation(value = "登录用户验证")
@PostMapping("doindex")
public RespBean doindex(UserLoginParam userLoginParam) {
String captcha = (String) redisUtil.get("captcha-"+userLoginParam.getUuid());
if (StringUtils.isEmpty(captcha)){
return RespBean.error("验证码过期重新刷新在试一吧");
}
// equalsIgnoreCase() 不区分大小写
if (!captcha.equalsIgnoreCase(userLoginParam.getCaptcha())){
return RespBean.error("验证码错误");
}
// ***********其它的比较和其它的登录流程***********************
// ........
return RespBean.success("登录成功");
}