Spring Boot图片验证码功能实现详解 - 从零开始到完美运行
📖 前言
大家好!今天我要和大家分享一个非常实用的功能:Spring Boot图片验证码。这个功能可以防止恶意攻击,比如暴力破解、刷票等。我们实现的是一个带有加减法运算的图片验证码,用户需要正确计算结果才能通过验证。
适合人群:Java初学者、Spring Boot新手、想要学习验证码实现的朋友
技术栈:Spring Boot 3.5.5 + Java 17 + Thymeleaf + Maven
🎯 功能预览
最终实现的效果:
- 生成随机数学表达式(如:5 + 3 = ?)
- 将表达式绘制成图片
- 用户输入计算结果
- 验证答案是否正确
- 支持刷新验证码
🛠️ 环境准备
1. 创建Spring Boot项目
首先,我们需要创建一个Spring Boot项目。我使用的是Spring Initializr创建的项目,包含以下依赖:
- Spring Web
- Thymeleaf
- Spring Data Redis
- Spring Session Data Redis
2. 项目结构
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── demo/
│ │ ├── Demo5Application.java
│ │ ├── config/
│ │ │ └── CorsConfig.java
│ │ ├── controller/
│ │ │ └── CaptchaController.java
│ │ ├── service/
│ │ │ ├── CaptchaService.java
│ │ │ └── MemoryCaptchaService.java
│ │ └── util/
│ │ └── CaptchaUtil.java
│ └── resources/
│ ├── application.properties
│ └── templates/
│ └── index.html
└── test/
└── java/
└── com/
└── example/
└── demo/
└── CaptchaUtilTest.java
第一步:配置Maven依赖
首先,我们需要在pom.xml
中添加必要的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>demo5</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo5</name>
<description>demo5</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Redis支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Session Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
解释:
spring-boot-starter-web
:提供Web开发基础功能spring-boot-starter-thymeleaf
:模板引擎,用于渲染HTML页面spring-boot-starter-data-redis
:Redis数据库支持spring-session-data-redis
:将Session存储到Redis中
第二步:创建验证码工具类
这是整个功能的核心!我们创建一个工具类来生成数学表达式和绘制图片:
package com.example.demo.util;
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;
/**
* 验证码工具类
* 用于生成数学表达式验证码图片
*/
public class CaptchaUtil {
/**
* 生成随机数学表达式和答案
* @return MathExpression 包含表达式和答案的对象
*/
public static MathExpression generateMathExpression() {
Random random = new Random();
int a = random.nextInt(10) + 1; // 1-10
int b = random.nextInt(10) + 1; // 1-10
// 随机决定是加法还是减法
String operator;
int result;
if (random.nextBoolean()) {
operator = "+";
result = a + b;
} else {
operator = "-";
// 确保结果不为负数
if (a < b) {
int temp = a;
a = b;
b = temp;
}
result = a - b;
}
String expression = a + " " + operator + " " + b + " = ?";
return new MathExpression(expression, result);
}
/**
* 生成验证码图片
* @param expression 数学表达式
* @return Base64编码的图片字符串
* @throws IOException IO异常
*/
public static String generateCaptchaImage(String expression) throws IOException {
int width = 120;
int height = 40;
// 创建图片对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 设置背景色为白色
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
// 设置字体
g.setFont(new Font("Arial", Font.BOLD, 16));
// 绘制干扰线(让验证码更难被机器识别)
Random random = new Random();
for (int i = 0; i < 5; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
g.drawLine(x1, y1, x2, y2);
}
// 绘制表达式文字
g.setColor(Color.BLACK);
g.drawString(expression, 10, 25);
g.dispose();
// 将图片转换为Base64字符串
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
byte[] bytes = baos.toByteArray();
return "data:image/png;base64," + Base64.getEncoder().encodeToString(bytes);
}
/**
* 内部类用于存储表达式和结果
*/
public static class MathExpression {
private String expression;
private int result;
public MathExpression(String expression, int result) {
this.expression = expression;
this.result = result;
}
public String getExpression() {
return expression;
}
public int getResult() {
return result;
}
}
}
代码解释:
generateMathExpression()
方法:- 生成两个1-10的随机数
- 随机选择加法或减法
- 确保减法结果不为负数
- 返回表达式字符串和正确答案
generateCaptchaImage()
方法:- 创建120x40像素的图片
- 设置白色背景
- 绘制5条随机颜色的干扰线
- 在图片上绘制数学表达式
- 将图片转换为Base64字符串返回
MathExpression
内部类:- 封装表达式和答案
- 提供getter方法
第三步:创建验证码服务类
我们需要两个服务类:一个基于Session,一个基于内存存储。
3.1 基于Session的验证码服务
package com.example.demo.service;
import com.example.demo.util.CaptchaUtil;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
/**
* 验证码服务类
* 处理验证码的生成和验证逻辑
*/
@Service
public class CaptchaService {
/**
* 生成验证码并存入session
* @return Base64编码的验证码图片
* @throws IOException IO异常
*/
public String generateCaptcha() throws IOException {
CaptchaUtil.MathExpression mathExpression = CaptchaUtil.generateMathExpression();
String imageBase64 = CaptchaUtil.generateCaptchaImage(mathExpression.getExpression());
// 获取当前session并存储答案
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession();
session.setAttribute("captchaAnswer", mathExpression.getResult());
session.setMaxInactiveInterval(300); // 5分钟有效期
return imageBase64;
}
/**
* 验证用户输入的答案
* @param userAnswer 用户输入的答案
* @return 验证是否成功
*/
public boolean validateCaptcha(String userAnswer) {
try {
int answer = Integer.parseInt(userAnswer);
// 获取当前session中的答案
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession();
Integer correctAnswer = (Integer) session.getAttribute("captchaAnswer");
// 添加调试信息
System.out.println("用户输入答案: " + answer);
System.out.println("正确答案: " + correctAnswer);
System.out.println("Session ID: " + session.getId());
if (correctAnswer != null && answer == correctAnswer) {
// 验证成功后移除答案
session.removeAttribute("captchaAnswer");
System.out.println("验证成功");
return true;
}
System.out.println("验证失败");
return false;
} catch (NumberFormatException e) {
System.out.println("数字格式错误: " + userAnswer);
return false;
}
}
}
3.2 基于内存的验证码服务(解决跨域问题)
package com.example.demo.service;
import com.example.demo.util.CaptchaUtil;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于内存的验证码服务
* 解决跨域Session问题
*/
@Service
public class MemoryCaptchaService {
// 使用ConcurrentHashMap存储验证码,key为验证码ID,value为答案
private final Map<String, Integer> captchaStorage = new ConcurrentHashMap<>();
/**
* 生成验证码并返回验证码ID和图片
* @return CaptchaResponse 包含验证码ID和Base64图片
* @throws IOException IO异常
*/
public CaptchaResponse generateCaptcha() throws IOException {
CaptchaUtil.MathExpression mathExpression = CaptchaUtil.generateMathExpression();
String imageBase64 = CaptchaUtil.generateCaptchaImage(mathExpression.getExpression());
// 生成唯一验证码ID
String captchaId = UUID.randomUUID().toString();
// 存储答案到内存中
captchaStorage.put(captchaId, mathExpression.getResult());
System.out.println("生成验证码ID: " + captchaId + ", 答案: " + mathExpression.getResult());
return new CaptchaResponse(captchaId, imageBase64);
}
/**
* 验证用户输入的答案
* @param captchaId 验证码ID
* @param userAnswer 用户输入的答案
* @return 验证是否成功
*/
public boolean validateCaptcha(String captchaId, String userAnswer) {
try {
int answer = Integer.parseInt(userAnswer);
Integer correctAnswer = captchaStorage.get(captchaId);
System.out.println("验证码ID: " + captchaId);
System.out.println("用户输入答案: " + answer);
System.out.println("正确答案: " + correctAnswer);
if (correctAnswer != null && answer == correctAnswer) {
// 验证成功后移除验证码
captchaStorage.remove(captchaId);
System.out.println("验证成功");
return true;
}
System.out.println("验证失败");
return false;
} catch (NumberFormatException e) {
System.out.println("数字格式错误: " + userAnswer);
return false;
}
}
/**
* 验证码响应类
*/
public static class CaptchaResponse {
private String captchaId;
private String imageBase64;
public CaptchaResponse(String captchaId, String imageBase64) {
this.captchaId = captchaId;
this.imageBase64 = imageBase64;
}
public String getCaptchaId() {
return captchaId;
}
public String getImageBase64() {
return imageBase64;
}
}
}
两种服务类的区别:
- Session服务:将答案存储在HttpSession中,适合同域访问
- 内存服务:将答案存储在内存Map中,通过唯一ID关联,解决跨域问题
第四步:创建控制器
控制器负责处理HTTP请求,连接前端和后端:
package com.example.demo.controller;
import com.example.demo.service.CaptchaService;
import com.example.demo.service.MemoryCaptchaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 验证码控制器
* 处理验证码相关的HTTP请求
*/
@Controller
public class CaptchaController {
@Autowired
private CaptchaService captchaService;
@Autowired
private MemoryCaptchaService memoryCaptchaService;
/**
* 显示验证页面
* @param model 模型对象
* @return 页面名称
*/
@GetMapping("/")
public String index(Model model) {
return "index";
}
/**
* 获取验证码图片(基于Session)
* @return Base64编码的验证码图片
*/
@GetMapping("/captcha")
@ResponseBody
public ResponseEntity<String> getCaptcha() {
try {
String imageBase64 = captchaService.generateCaptcha();
return ResponseEntity.ok(imageBase64);
} catch (IOException e) {
return ResponseEntity.status(500).body("生成验证码失败");
}
}
/**
* 验证答案(基于Session)
* @param answer 用户输入的答案
* @return 验证结果
*/
@PostMapping("/validate")
@ResponseBody
public ResponseEntity<String> validateCaptcha(@RequestParam String answer) {
System.out.println("收到验证请求,答案: " + answer);
boolean isValid = captchaService.validateCaptcha(answer);
if (isValid) {
return ResponseEntity.ok("验证成功");
} else {
return ResponseEntity.badRequest().body("验证失败");
}
}
/**
* 获取基于内存的验证码(解决跨域Session问题)
* @return 包含验证码ID和图片的响应
*/
@GetMapping("/memory-captcha")
@ResponseBody
public ResponseEntity<Map<String, String>> getMemoryCaptcha() {
try {
MemoryCaptchaService.CaptchaResponse response = memoryCaptchaService.generateCaptcha();
Map<String, String> result = new HashMap<>();
result.put("captchaId", response.getCaptchaId());
result.put("imageData", response.getImageBase64());
return ResponseEntity.ok(result);
} catch (IOException e) {
return ResponseEntity.status(500).build();
}
}
/**
* 验证基于内存的验证码
* @param captchaId 验证码ID
* @param answer 用户输入的答案
* @return 验证结果
*/
@PostMapping("/memory-validate")
@ResponseBody
public ResponseEntity<String> validateMemoryCaptcha(
@RequestParam String captchaId,
@RequestParam String answer) {
System.out.println("收到内存验证码验证请求,ID: " + captchaId + ", 答案: " + answer);
boolean isValid = memoryCaptchaService.validateCaptcha(captchaId, answer);
if (isValid) {
return ResponseEntity.ok("验证成功");
} else {
return ResponseEntity.badRequest().body("验证失败");
}
}
}
控制器解释:
@GetMapping("/")
:显示主页面@GetMapping("/captcha")
:获取Session验证码@PostMapping("/validate")
:验证Session验证码@GetMapping("/memory-captcha")
:获取内存验证码@PostMapping("/memory-validate")
:验证内存验证码
第五步:创建前端页面
创建一个美观的HTML页面:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证码演示</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 50px;
background-color: #f5f5f5;
}
.container {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
#captchaImage {
margin-bottom: 10px;
border: 1px solid #ddd;
}
#refreshCaptcha {
margin-left: 10px;
background-color: #2196F3;
}
#refreshCaptcha:hover {
background-color: #1976D2;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
display: none;
}
.success {
background-color: #dff0d8;
color: #3c763d;
border: 1px solid #d6e9c6;
}
.error {
background-color: #f2dede;
color: #a94442;
border: 1px solid #ebccd1;
}
.captcha-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>验证码验证</h2>
<div class="form-group">
<label>验证码:</label>
<div class="captcha-container">
<img id="captchaImage" src="" alt="验证码">
<button id="refreshCaptcha">刷新</button>
</div>
</div>
<div class="form-group">
<label for="answer">请输入计算结果:</label>
<input type="text" id="answer" placeholder="请输入计算结果">
</div>
<!-- 隐藏的验证码ID字段 -->
<input type="hidden" id="captchaId" value="">
<button id="submitBtn">提交验证</button>
<div id="message" class="message"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const captchaImage = document.getElementById('captchaImage');
const refreshBtn = document.getElementById('refreshCaptcha');
const answerInput = document.getElementById('answer');
const submitBtn = document.getElementById('submitBtn');
const messageDiv = document.getElementById('message');
const captchaIdInput = document.getElementById('captchaId');
// 加载验证码
function loadCaptcha() {
fetch('http://localhost:8080/memory-captcha')
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => {
captchaImage.src = data.imageData;
captchaIdInput.value = data.captchaId;
console.log('验证码ID:', data.captchaId);
})
.catch(error => {
console.error('加载验证码失败:', error);
showMessage('加载验证码失败,请重试', 'error');
});
}
// 初始化加载验证码
loadCaptcha();
// 刷新验证码
refreshBtn.addEventListener('click', function() {
loadCaptcha();
answerInput.value = '';
messageDiv.style.display = 'none';
});
// 提交验证
submitBtn.addEventListener('click', function() {
const answer = answerInput.value.trim();
const captchaId = captchaIdInput.value;
if (!answer) {
showMessage('请输入计算结果', 'error');
return;
}
if (!captchaId) {
showMessage('验证码已过期,请刷新', 'error');
loadCaptcha();
return;
}
// 发送验证请求
const formData = new FormData();
formData.append('captchaId', captchaId);
formData.append('answer', answer);
fetch('http://localhost:8080/memory-validate', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
return response.text();
} else {
throw new Error('验证失败');
}
})
.then(result => {
showMessage('验证成功!', 'success');
})
.catch(error => {
showMessage('验证失败,请重试', 'error');
loadCaptcha(); // 刷新验证码
answerInput.value = '';
});
});
// 显示消息
function showMessage(message, type) {
messageDiv.textContent = message;
messageDiv.className = 'message ' + type;
messageDiv.style.display = 'block';
}
});
</script>
</body>
</html>
前端功能解释:
- 页面加载时:自动获取验证码图片
- 刷新按钮:重新获取新的验证码
- 提交验证:发送用户输入的答案到后端验证
- 消息提示:显示验证成功或失败的消息
⚙️ 第六步:配置CORS和应用程序
6.1 CORS配置(解决跨域问题)
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* CORS配置类
* 解决跨域请求问题
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
6.2 应用程序配置
# application.properties
spring.application.name=demo5
# 服务器配置
server.port=8080
# Session配置
server.servlet.session.timeout=300s
# Redis配置(可选,如果不使用Redis可以注释掉)
# spring.redis.host=localhost
# spring.redis.port=6379
# spring.redis.password=
# spring.redis.database=0
# 使用Redis存储Session(可选)
# spring.session.store-type=redis
# 日志配置
logging.level.com.example.demo=DEBUG
第七步:创建测试类
为了确保我们的代码正常工作,我们创建一个测试类:
package com.example.demo;
import com.example.demo.util.CaptchaUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* 验证码工具类测试
*/
public class CaptchaUtilTest {
@Test
public void testGenerateMathExpression() {
// 测试生成数学表达式
CaptchaUtil.MathExpression expression = CaptchaUtil.generateMathExpression();
assertNotNull(expression);
assertNotNull(expression.getExpression());
assertTrue(expression.getExpression().contains("="));
assertTrue(expression.getExpression().contains("?"));
assertTrue(expression.getResult() >= 0); // 结果应该非负
}
@Test
public void testGenerateCaptchaImage() throws Exception {
// 测试生成验证码图片
String expression = "5 + 3 = ?";
String imageBase64 = CaptchaUtil.generateCaptchaImage(expression);
assertNotNull(imageBase64);
assertTrue(imageBase64.startsWith("data:image/png;base64,"));
assertTrue(imageBase64.length() > 100); // Base64字符串应该有一定长度
}
@Test
public void testMathExpressionClass() {
// 测试MathExpression内部类
String expression = "2 + 3 = ?";
int result = 5;
CaptchaUtil.MathExpression mathExpression = new CaptchaUtil.MathExpression(expression, result);
assertEquals(expression, mathExpression.getExpression());
assertEquals(result, mathExpression.getResult());
}
}
第八步:运行和测试
8.1 启动应用
- 在IDE中运行
Demo5Application.java
- 或者使用命令行:
mvn spring-boot:run
- 应用启动后访问:
http://localhost:8080
8.2 测试功能
- 页面加载:自动显示验证码图片
- 计算答案:根据图片中的数学表达式计算答案
- 输入答案:在输入框中输入计算结果
- 提交验证:点击"提交验证"按钮
- 刷新验证码:点击"刷新"按钮获取新的验证码
遇到的问题和解决方案
问题1:HttpSession无法解析
错误信息:
无法解析符号 'HttpSession'
原因:Spring Boot 3.x使用Jakarta EE,javax.servlet
包被替换为jakarta.servlet
解决方案:
// 错误的导入
import javax.servlet.http.HttpSession;
// 正确的导入
import jakarta.servlet.http.HttpSession;
问题2:CORS跨域问题
错误信息:
Access to fetch at 'http://localhost:8080/captcha' from origin 'null' has been blocked by CORS policy
原因:当通过IDE预览页面时,页面运行在不同端口,浏览器阻止跨域请求
解决方案:
- 创建CORS配置类
- 使用内存验证码服务替代Session验证码
- 前端使用绝对URL请求
问题3:Session无法跨域共享
错误信息:
验证总是失败,Session中的答案为空
原因:跨域请求时Session无法正确共享
解决方案:
- 创建基于内存的验证码服务
- 使用唯一ID关联验证码和答案
- 前端传递验证码ID进行验证
问题4:Maven命令无法识别
错误信息:
mvn : 无法将"mvn"项识别为 cmdlet、函数、脚本文件或可运行程序的名称
原因:Maven没有安装或没有配置环境变量
解决方案:
- 安装Maven并配置环境变量
- 或者直接使用IDE运行应用
- 或者使用项目自带的Maven Wrapper:
./mvnw spring-boot:run
📊 功能特点总结
✅ 已实现的功能
- 数学表达式验证码:比传统字符验证码更友好
- 图片生成:自动绘制验证码图片
- 双重存储方案:支持Session和内存两种存储方式
- 跨域支持:解决了IDE预览时的跨域问题
- 美观界面:现代化的UI设计
- 调试信息:控制台输出详细的验证过程
- 自动刷新:验证失败后自动刷新验证码
🎯 技术亮点
- Base64图片编码:直接在前端显示图片
- ConcurrentHashMap:线程安全的内存存储
- UUID唯一标识:确保验证码ID的唯一性
- CORS配置:完整的跨域解决方案
- 异常处理:完善的错误处理机制
🔮 扩展功能建议
1. 添加Redis存储
// 可以扩展为使用Redis存储验证码
@Autowired
private StringRedisTemplate redisTemplate;
public void storeCaptcha(String captchaId, int answer) {
redisTemplate.opsForValue().set("captcha:" + captchaId,
String.valueOf(answer), 5, TimeUnit.MINUTES);
}
2. 增加验证码复杂度
// 可以添加更多运算符
String[] operators = {"+", "-", "*", "/"};
// 可以增加数字范围
int a = random.nextInt(20) + 1; // 1-20
3. 添加验证码过期机制
// 可以添加定时清理过期验证码
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void cleanExpiredCaptchas() {
// 清理逻辑
}
4. 增加验证码样式
// 可以添加更多干扰元素
g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
g.fillOval(random.nextInt(width), random.nextInt(height), 10, 10);
学习总结
通过这个项目,我们学会了:
- Spring Boot基础:如何创建Web应用
- 图片处理:使用Java Graphics API绘制图片
- Base64编码:图片的编码和解码
- Session管理:HttpSession的使用
- 跨域问题:CORS的配置和解决
- 前端交互:JavaScript与后端API的交互
- 异常处理:完善的错误处理机制
- 测试驱动:编写单元测试验证功能
结语
恭喜你!你已经成功实现了一个完整的Spring Boot图片验证码功能。这个项目涵盖了:
- ✅ 后端API开发
- ✅ 图片生成和处理
- ✅ 前端页面开发
- ✅ 跨域问题解决
- ✅ 异常处理
- ✅ 单元测试
这个验证码功能可以应用到实际的Web项目中,有效防止恶意攻击。希望这个教程对你有帮助!
如果你有任何问题或建议,欢迎在评论区留言讨论!
项目源码:所有代码都已经在文章中完整提供,可以直接复制使用。
运行环境:Java 17 + Spring Boot 3.5.5 + Maven
测试地址:http://localhost:8080
祝学习愉快!🚀