⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/weixin_47121832 「CoderJoon」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

大家好!今天我要和大家分享一个非常实用的功能: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;
}
}
}

代码解释:

1.generateMathExpression()方法:

  • 生成两个1-10的随机数
  • 随机选择加法或减法
  • 确保减法结果不为负数
  • 返回表达式字符串和正确答案

2.generateCaptchaImage()方法:

  • 创建120x40像素的图片
  • 设置白色背景
  • 绘制5条随机颜色的干扰线
  • 在图片上绘制数学表达式
  • 将图片转换为Base64字符串返回

3.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项目中,有效防止恶意攻击。希望这个教程对你有帮助!

文章目录
  1. 1. 前言
  2. 2. 功能预览
  3. 3. 环境准备
    1. 3.0.1. 1. 创建Spring Boot项目
    2. 3.0.2. 2. 项目结构
  • 4. 第一步:配置Maven依赖
    1. 4.0.0.1. 解释:
  • 5. 第二步:创建验证码工具类
    1. 5.0.0.1. 代码解释:
  • 6. 第三步:创建验证码服务类
    1. 6.0.1. 3.1 基于Session的验证码服务
    2. 6.0.2. 3.2 基于内存的验证码服务(解决跨域问题)
  • 7. 第四步:创建控制器
    1. 7.0.0.1. 控制器解释:
  • 8. 第五步:创建前端页面
    1. 8.0.0.1. 前端功能解释:
  • 9. 第六步:配置CORS和应用程序
    1. 9.0.1. 6.1 CORS配置(解决跨域问题)
    2. 9.0.2. 6.2 应用程序配置
  • 10. 第七步:创建测试类
  • 11. 第八步:运行和测试
    1. 11.0.1. 8.1 启动应用
    2. 11.0.2. 8.2 测试功能
  • 12. 遇到的问题和解决方案
    1. 12.0.1. 问题1:HttpSession无法解析
    2. 12.0.2. 问题2:CORS跨域问题
    3. 12.0.3. 问题3:Session无法跨域共享
    4. 12.0.4. 问题4:Maven命令无法识别
  • 13. 功能特点总结
    1. 13.0.1. 已实现的功能
    2. 13.0.2. 技术亮点
  • 14. 扩展功能建议
    1. 14.0.1. 1. 添加Redis存储
    2. 14.0.2. 2. 增加验证码复杂度
    3. 14.0.3. 3. 添加验证码过期机制
    4. 14.0.4. 4. 增加验证码样式
  • 15. 学习总结
  • 16. 结语