外观
一个简单的登录图片验证码实现逻辑
前几天小 A 给我说,项目中需要加一个图片验证码逻辑,今天分享一下。
一、图形验证码的好处
- 防止自动化机器人攻击
- 降低账号被盗风险
- 限制一些高频操作
二、实现逻辑

效果:

接口返回了 base64 的一个图片,前端进行渲染即可:

三、核心代码实现
1. 验证码工具类 CaptchaUtil.java
java
import lombok.extern.slf4j.Slf4j;
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.Random;
/**
* 验证码生成工具类
*/
@Slf4j
public class CaptchaUtil {
// 验证码字符集
private static final char[] CHARS = {
'2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
};
// 验证码长度
private static final int CODE_LENGTH = 4;
// 图片宽度
private static final int IMAGE_WIDTH = 120;
// 图片高度
private static final int IMAGE_HEIGHT = 40;
// 干扰线数量
private static final int LINE_COUNT = 6;
// 干扰点数量
private static final int DOT_COUNT = 50;
private static final Random random = new Random();
/**
* 生成验证码和图片
*/
public static CaptchaResult generateCaptcha() {
// 生成验证码字符串
String captchaCode = generateCode();
// 创建图片缓冲区
BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色
g.setColor(Color.WHITE);
g.fillRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 28));
// 绘制验证码字符
drawCode(g, captchaCode);
// 绘制干扰线
drawLines(g);
// 绘制干扰点
drawDots(g);
g.dispose();
// 转换为Base64
String imageBase64 = imageToBase64(image);
return new CaptchaResult(captchaCode, imageBase64);
}
/**
* 生成随机验证码
*/
private static String generateCode() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
sb.append(CHARS[random.nextInt(CHARS.length)]);
}
return sb.toString();
}
/**
* 绘制验证码字符
*/
private static void drawCode(Graphics2D g, String code) {
int charWidth = IMAGE_WIDTH / (CODE_LENGTH + 1);
for (int i = 0; i < code.length(); i++) {
// 随机颜色
g.setColor(getRandomColor(100, 150));
// 随机角度 (-30° 到 30°)
double angle = Math.toRadians(random.nextInt(60) - 30);
g.rotate(angle, 20 + i * charWidth, 25);
// 绘制字符
g.drawString(String.valueOf(code.charAt(i)), 15 + i * charWidth, 28);
// 恢复角度
g.rotate(-angle, 20 + i * charWidth, 25);
}
}
/**
* 绘制干扰线
*/
private static void drawLines(Graphics2D g) {
for (int i = 0; i < LINE_COUNT; i++) {
g.setColor(getRandomColor(150, 200));
int x1 = random.nextInt(IMAGE_WIDTH);
int y1 = random.nextInt(IMAGE_HEIGHT);
int x2 = random.nextInt(IMAGE_WIDTH);
int y2 = random.nextInt(IMAGE_HEIGHT);
g.drawLine(x1, y1, x2, y2);
}
}
/**
* 绘制干扰点
*/
private static void drawDots(Graphics2D g) {
for (int i = 0; i < DOT_COUNT; i++) {
int x = random.nextInt(IMAGE_WIDTH);
int y = random.nextInt(IMAGE_HEIGHT);
g.setColor(getRandomColor(150, 200));
g.drawLine(x, y, x, y);
}
}
/**
* 生成随机颜色
*/
private static Color getRandomColor(int fc, int bc) {
if (fc > 255) fc = 255;
if (bc > 255) bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
/**
* 图片转Base64
*/
private static String imageToBase64(BufferedImage image) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", baos);
byte[] bytes = baos.toByteArray();
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
log.error("验证码图片转换Base64失败", e);
return "";
}
}
}2. 调用方代码 SysCaptchaService.java
java
import cn.hutool.core.lang.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class SysCaptchaService {
private final RedisUtils redisUtils;
// 验证码缓存前缀
private static final String CAPTCHA_KEY_PREFIX = "captcha:";
// 验证码过期时间(5分钟)
private static final long CAPTCHA_EXPIRE = 5 * 60;
/**
* 生成验证码
* @return 验证码信息
*/
public CaptchaDTO generateCaptcha() {
CaptchaResult captchaResult = CaptchaUtil.generateCaptcha();
String code = captchaResult.getCode();
String imageBase64 = captchaResult.getImageBase64();
// 生成UUID作为验证码标识
String uuid = UUID.randomUUID().toString().replace("-", "");
redisUtils.setBySetTime(CAPTCHA_KEY_PREFIX + uuid, code, CAPTCHA_EXPIRE, TimeUnit.SECONDS);
return CaptchaDTO.builder().uuid(uuid).image(imageBase64).build();
}
/**
* 验证验证码
*/
public void validateCaptcha(String uuid, String inputCode) {
if (uuid == null || inputCode == null) {
throw new RuntimeException("请输入验证码");
}
String key = CAPTCHA_KEY_PREFIX + uuid;
String storedCode = redisUtils.get(key, String.class);
if (storedCode == null) {
throw new RuntimeException("验证码已过期");
}
// 验证成功后删除验证码
if (storedCode.equalsIgnoreCase(inputCode)) {
redisUtils.delete(key);
} else {
throw new RuntimeException("验证码错误");
}
}
}到这里恭喜你,已经实现了图片验证码逻辑。