Spring Boot图片验证码功能实现详解 - 从零开始到完美运行

发布于:2025-09-12 ⋅ 阅读:(13) ⋅ 点赞:(0)

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;
        }
    }
}

代码解释

  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;
        }
    }
}

两种服务类的区别

  1. Session服务:将答案存储在HttpSession中,适合同域访问
  2. 内存服务:将答案存储在内存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>

前端功能解释

  1. 页面加载时:自动获取验证码图片
  2. 刷新按钮:重新获取新的验证码
  3. 提交验证:发送用户输入的答案到后端验证
  4. 消息提示:显示验证成功或失败的消息

⚙️ 第六步:配置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 启动应用

  1. 在IDE中运行Demo5Application.java
  2. 或者使用命令行:mvn spring-boot:run
  3. 应用启动后访问:http://localhost:8080

8.2 测试功能

  1. 页面加载:自动显示验证码图片
  2. 计算答案:根据图片中的数学表达式计算答案
  3. 输入答案:在输入框中输入计算结果
  4. 提交验证:点击"提交验证"按钮
  5. 刷新验证码:点击"刷新"按钮获取新的验证码

遇到的问题和解决方案

问题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预览页面时,页面运行在不同端口,浏览器阻止跨域请求

解决方案

  1. 创建CORS配置类
  2. 使用内存验证码服务替代Session验证码
  3. 前端使用绝对URL请求

问题3:Session无法跨域共享

错误信息

验证总是失败,Session中的答案为空

原因:跨域请求时Session无法正确共享

解决方案

  1. 创建基于内存的验证码服务
  2. 使用唯一ID关联验证码和答案
  3. 前端传递验证码ID进行验证

问题4:Maven命令无法识别

错误信息

mvn : 无法将"mvn"项识别为 cmdlet、函数、脚本文件或可运行程序的名称

原因:Maven没有安装或没有配置环境变量

解决方案

  1. 安装Maven并配置环境变量
  2. 或者直接使用IDE运行应用
  3. 或者使用项目自带的Maven Wrapper:./mvnw spring-boot:run

📊 功能特点总结

✅ 已实现的功能

  1. 数学表达式验证码:比传统字符验证码更友好
  2. 图片生成:自动绘制验证码图片
  3. 双重存储方案:支持Session和内存两种存储方式
  4. 跨域支持:解决了IDE预览时的跨域问题
  5. 美观界面:现代化的UI设计
  6. 调试信息:控制台输出详细的验证过程
  7. 自动刷新:验证失败后自动刷新验证码

🎯 技术亮点

  1. Base64图片编码:直接在前端显示图片
  2. ConcurrentHashMap:线程安全的内存存储
  3. UUID唯一标识:确保验证码ID的唯一性
  4. CORS配置:完整的跨域解决方案
  5. 异常处理:完善的错误处理机制

🔮 扩展功能建议

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);

学习总结

通过这个项目,我们学会了:

  1. Spring Boot基础:如何创建Web应用
  2. 图片处理:使用Java Graphics API绘制图片
  3. Base64编码:图片的编码和解码
  4. Session管理:HttpSession的使用
  5. 跨域问题:CORS的配置和解决
  6. 前端交互:JavaScript与后端API的交互
  7. 异常处理:完善的错误处理机制
  8. 测试驱动:编写单元测试验证功能

结语

恭喜你!你已经成功实现了一个完整的Spring Boot图片验证码功能。这个项目涵盖了:

  • ✅ 后端API开发
  • ✅ 图片生成和处理
  • ✅ 前端页面开发
  • ✅ 跨域问题解决
  • ✅ 异常处理
  • ✅ 单元测试

这个验证码功能可以应用到实际的Web项目中,有效防止恶意攻击。希望这个教程对你有帮助!

如果你有任何问题或建议,欢迎在评论区留言讨论!


项目源码:所有代码都已经在文章中完整提供,可以直接复制使用。

运行环境:Java 17 + Spring Boot 3.5.5 + Maven

测试地址:http://localhost:8080

祝学习愉快!🚀


网站公告

今日签到

点亮在社区的每一天
去签到