2026/3/24 2:59:47
网站建设
项目流程
响应式网站手机,什么软件做网站链接,百度网站建设公司,用python做网站不常见前言
大家好#xff01;今天我要和大家分享一个非常实用的功能#xff1a;Spring Boot图片验证码。这个功能可以防止恶意攻击#xff0c;比如暴力破解、刷票等。我们实现的是一个带有加减法运算的图片验证码#xff0c;用户需要正确计算结果才能通过验证。
适合人群…前言大家好今天我要和大家分享一个非常实用的功能Spring Boot图片验证码。这个功能可以防止恶意攻击比如暴力破解、刷票等。我们实现的是一个带有加减法运算的图片验证码用户需要正确计算结果才能通过验证。适合人群Java初学者、Spring Boot新手、想要学习验证码实现的朋友技术栈Spring Boot 3.5.5 Java 17 Thymeleaf Maven功能预览最终实现的效果生成随机数学表达式如5 3 ?将表达式绘制成图片用户输入计算结果验证答案是否正确支持刷新验证码环境准备1. 创建Spring Boot项目首先我们需要创建一个Spring Boot项目。我使用的是Spring Initializr创建的项目包含以下依赖Spring WebThymeleafSpring Data RedisSpring Session Data Redis2. 项目结构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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.5/version relativePath/!-- lookup parent from repository -- /parent groupIdorg.example/groupId artifactIddemo5/artifactId version0.0.1-SNAPSHOT/version namedemo5/name descriptiondemo5/description properties java.version17/java.version /properties dependencies !-- Spring Boot Web Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- Redis支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- Spring Session Redis -- dependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependency !-- 测试依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project解释spring-boot-starter-web提供Web开发基础功能spring-boot-starter-thymeleaf模板引擎用于渲染HTML页面spring-boot-starter-data-redisRedis数据库支持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; /** * 验证码工具类 * 用于生成数学表达式验证码图片 */ publicclass 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 ?; returnnew 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(); returndata:image/png;base64, Base64.getEncoder().encodeToString(bytes); } /** * 内部类用于存储表达式和结果 */ publicstaticclass MathExpression { private String expression; privateint 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 publicclass 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(验证成功); returntrue; } System.out.println(验证失败); returnfalse; } catch (NumberFormatException e) { System.out.println(数字格式错误: userAnswer); returnfalse; } } }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 publicclass MemoryCaptchaService { // 使用ConcurrentHashMap存储验证码key为验证码IDvalue为答案 privatefinal MapString, 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()); returnnew 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(验证成功); returntrue; } System.out.println(验证失败); returnfalse; } catch (NumberFormatException e) { System.out.println(数字格式错误: userAnswer); returnfalse; } } /** * 验证码响应类 */ publicstaticclass 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 publicclass CaptchaController { Autowired private CaptchaService captchaService; Autowired private MemoryCaptchaService memoryCaptchaService; /** * 显示验证页面 * param model 模型对象 * return 页面名称 */ GetMapping(/) public String index(Model model) { returnindex; } /** * 获取验证码图片基于Session * return Base64编码的验证码图片 */ GetMapping(/captcha) ResponseBody public ResponseEntityString 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 ResponseEntityString 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 ResponseEntityMapString, String getMemoryCaptcha() { try { MemoryCaptchaService.CaptchaResponse response memoryCaptchaService.generateCaptcha(); MapString, 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 ResponseEntityString 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:thhttp://www.thymeleaf.org head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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: 0010pxrgba(0,0,0,0.1); } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input[typetext] { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } button { padding: 10px15px; 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 classcontainer h2验证码验证/h2 div classform-group label验证码/label div classcaptcha-container img idcaptchaImage src alt验证码 button idrefreshCaptcha刷新/button /div /div div classform-group label foranswer请输入计算结果/label input typetext idanswer placeholder请输入计算结果 /div !-- 隐藏的验证码ID字段 -- input typehidden idcaptchaId value button idsubmitBtn提交验证/button div idmessage classmessage/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); // 加载验证码 functionloadCaptcha() { fetch(http://localhost:8080/memory-captcha) .then(response { if (!response.ok) { thrownewError(网络响应不正常); } 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 { thrownewError(验证失败); } }) .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 publicclass 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); returnsource; } }6.2 应用程序配置# application.properties spring.application.namedemo5 # 服务器配置 server.port8080 # Session配置 server.servlet.session.timeout300s # Redis配置可选如果不使用Redis可以注释掉 # spring.redis.hostlocalhost # spring.redis.port6379 # spring.redis.password # spring.redis.database0 # 使用Redis存储Session可选 # spring.session.store-typeredis # 日志配置 logging.level.com.example.demoDEBUG第七步创建测试类为了确保我们的代码正常工作我们创建一个测试类package com.example.demo; import com.example.demo.util.CaptchaUtil; import org.junit.jupiter.api.Test; importstatic org.junit.jupiter.api.Assertions.*; /** * 验证码工具类测试 */ publicclass 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:80808.2 测试功能页面加载自动显示验证码图片计算答案根据图片中的数学表达式计算答案输入答案在输入框中输入计算结果提交验证点击提交验证按钮刷新验证码点击刷新按钮获取新的验证码遇到的问题和解决方案问题1HttpSession无法解析错误信息无法解析符号 HttpSession原因Spring Boot 3.x使用Jakarta EEjavax.servlet包被替换为jakarta.servlet解决方案// 错误的导入 import javax.servlet.http.HttpSession; // 正确的导入 import jakarta.servlet.http.HttpSession;问题2CORS跨域问题错误信息Access to fetch at http://localhost:8080/captcha from origin null has been blocked by CORS policy原因当通过IDE预览页面时页面运行在不同端口浏览器阻止跨域请求解决方案创建CORS配置类使用内存验证码服务替代Session验证码前端使用绝对URL请求问题3Session无法跨域共享错误信息验证总是失败Session中的答案为空原因跨域请求时Session无法正确共享解决方案创建基于内存的验证码服务使用唯一ID关联验证码和答案前端传递验证码ID进行验证问题4Maven命令无法识别错误信息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-203. 添加验证码过期机制// 可以添加定时清理过期验证码 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项目中有效防止恶意攻击。希望这个教程对你有帮助