news 2026/2/15 18:53:38

JSP+Servlet结合验证码防止表单重复提交

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet结合验证码防止表单重复提交

使用验证码防止表单重复提交:基于 JSP + Servlet 的实战方案

在开发 Web 应用时,你有没有遇到过用户疯狂点击“提交”按钮导致服务雪崩的情况?尤其是在涉及高计算成本的操作中,比如 AI 图像生成、订单支付或注册流程,这种行为轻则造成资源浪费,重则直接拖垮服务器。

最近我在体验阿里推出的Z-Image-ComfyUI文生图工具时就碰到了这个问题。这个模型基于 60 亿参数的 Z-Image 大模型构建,支持中英文输入、细节还原精准,推理质量非常高——但代价也很明显:一次图像生成可能消耗数秒 GPU 时间和几 GB 显存。

如果用户因为页面没反应连续点了十次“生成”,后台就得处理十个几乎相同的任务请求。更糟的是,这些请求还可能是来自脚本的恶意刷调用。这时候,光靠前端禁用按钮是不够的,我们必须从服务端入手,建立真正的防护机制。

今天我们就来实现一套完整的防重复提交方案,使用经典的JSP + Servlet技术栈,结合验证码机制,确保每个操作只能成功执行一次。


验证码不只是为了防机器人

很多人觉得验证码是个“反人类”的设计,但它其实是一种非常有效的安全控制手段。除了识别机器流量外,它还能很好地解决人为误操作带来的重复请求问题

核心思路很简单:

  • 每次页面加载时生成一个唯一的验证码,并存储在用户的 Session 中;
  • 用户提交表单时必须携带该验证码;
  • 后端比对验证码是否匹配;
  • 一旦验证通过,立即清除 Session 中的验证码,使其失效。

这样一来,即使用户快速连点多次,也只有第一次能成功,后续所有请求都会因“验证码无效”而被拒绝。

这就像给每张火车票打上“已检票”戳一样——进站后就不能再用了。


自定义字体支持中文验证码

为了让验证码看起来更美观,特别是支持中文字符显示(虽然我们这里用的是字母数字组合),我们可以封装一个字体加载工具类。

package org.zimage.util; import java.io.ByteArrayInputStream; import java.awt.Font; /** * 自定义字体加载工具类(用于验证码中文渲染) */ public class ImgFontByte { public Font getFont(int fontHeight) { try { byte[] fontBytes = getFontData(); Font baseFont = Font.createFont(Font.TRUETYPE_FONT, new ByteArrayInputStream(fontBytes)); return baseFont.deriveFont(Font.PLAIN, fontHeight); } catch (Exception e) { return new Font("Arial", Font.PLAIN, fontHeight); } } /** * 返回嵌入式字体数据(简化版,实际项目可替换为真实ttf文件读取) */ private byte[] getFontData() { // 注意:此处应放入真实的TTF字节码,或改为读取classpath下的资源文件 // 示例中省略具体字节数组以保持简洁 return new byte[0]; } }

💡 提示:如果你希望验证码中包含中文字符(如“验”“证”“码”等),建议将一份轻量级中文字体(如思源黑体精简版)转为字节数组嵌入代码,或通过ClassLoader.getResourceAsStream()动态加载。


实现验证码图片生成器

接下来我们编写一个验证码绘图类,负责生成带干扰线和随机字符的 JPEG 图片。

package org.zimage.servlet; import org.zimage.util.ImgFontByte; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; import java.util.Random; /** * 验证码生成工具类 */ public class ValidateCode { private int width = 120; // 图片宽 private int height = 40; // 图片高 private int codeCount = 4; // 验证码长度 private int lineCount = 50; // 干扰线条数 private String code; // 当前验证码字符串 private BufferedImage buffImg; // 缓存图像 // 可选字符集(去除了容易混淆的0、O、I、l等) private static final char[] CODE_SEQUENCE = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '8', '9' }; public ValidateCode() { this(120, 40, 4, 50); } public ValidateCode(int width, int height, int codeCount, int lineCount) { this.width = width; this.height = height; this.codeCount = codeCount; this.lineCount = lineCount; createCode(); } private void createCode() { int x = width / (codeCount + 2); int fontHeight = height - 6; int codeY = height - 6; // 创建图像缓冲区 buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = buffImg.createGraphics(); // 设置背景为白色 g.setColor(Color.WHITE); g.fillRect(0, 0, width, height); // 加载字体 ImgFontByte imgFont = new ImgFontByte(); Font font = imgFont.getFont(fontHeight); g.setFont(font); Random random = new Random(); // 绘制干扰线 for (int i = 0; i < lineCount; i++) { int xs = random.nextInt(width); int ys = random.nextInt(height); int xe = xs + random.nextInt(width >> 3); int ye = ys + random.nextInt(height >> 3); int red = random.nextInt(255); int green = random.nextInt(255); int blue = random.nextInt(255); g.setColor(new Color(red, green, blue)); g.drawLine(xs, ys, xe, ye); } // 生成验证码文本 StringBuilder sb = new StringBuilder(); for (int i = 0; i < codeCount; i++) { char c = CODE_SEQUENCE[random.nextInt(CODE_SEQUENCE.length)]; sb.append(c); // 每个字符颜色不同 int red = random.nextInt(180); int green = random.nextInt(180); int blue = random.nextInt(180); g.setColor(new Color(red, green, blue)); g.drawString(String.valueOf(c), (i + 1) * x, codeY); } code = sb.toString(); } public void write(OutputStream os) throws IOException { ImageIO.write(buffImg, "jpeg", os); os.flush(); os.close(); } public String getCode() { return code; } public BufferedImage getBuffImg() { return buffImg; } }

这个类不仅生成了视觉上难以被 OCR 自动识别的验证码图像,还通过随机颜色、偏移位置和干扰线提升了安全性。


提供验证码图片接口

我们需要一个 Servlet 来输出这张图片,并把正确的验证码保存到当前用户的 Session 中。

package org.zimage.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/vcode") public class ValidateCodeServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置响应头:告诉浏览器这是张图片,不要缓存 resp.setContentType("image/jpeg"); resp.setHeader("Pragma", "no-cache"); resp.setHeader("Cache-Control", "no-cache"); resp.setDateHeader("Expires", 0); // 获取 session HttpSession session = req.getSession(); // 创建验证码对象 ValidateCode vCode = new ValidateCode(120, 40, 4, 50); // 将验证码文本保存到 session session.setAttribute("verify_code", vCode.getCode()); // 输出图片到客户端 vCode.write(resp.getOutputStream()); } }

每次访问/vcode路径时,都会刷新验证码并更新 Session 值。前端可以通过加时间戳的方式强制刷新图片,避免浏览器缓存。


构建前端交互页面

下面是用户界面generate.jsp,允许用户输入提示词并填写验证码。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; %> <!DOCTYPE html> <html> <head> <base href="<%=basePath%>"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Z-Image AI绘图</title> <script type="text/javascript"> window.onload = function () { var img = document.getElementById("vcode_img"); img.onclick = function () { this.src = "vcode?date=" + new Date().getTime(); }; } </script> </head> <body> <h2 style="color:#333;">🎨 使用 Z-Image 生成你的专属图像</h2> <form action="Generate" method="post"> 提示词:<input type="text" name="prompt" placeholder="例如:一只戴着墨镜的猫,在月球上冲浪" style="width:300px"/><br><br> 验证码:<input type="text" name="vcode" maxlength="4" style="width:60px"/>&nbsp; <img src="vcode" id="vcode_img" alt="点击刷新" style="cursor:pointer;"><br><br> <input type="submit" value="立即生成" /> </form> <!-- 显示结果 --> <% String msg = (String) request.getAttribute("msg"); if (msg != null) { %> <div style="margin-top:20px; color:red; font-weight:bold;"> <%= msg %> </div> <% } %> </body> </html>

页面加载时自动获取验证码图片,点击图片即可刷新。表单提交后由GenerateServlet处理。


核心逻辑:防止重复提交的关键一步

这是整个机制中最关键的部分——验证与销毁验证码

package org.zimage.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/Generate") public class GenerateServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 获取用户输入 String prompt = req.getParameter("prompt"); String userInputCode = req.getParameter("vcode"); HttpSession session = req.getSession(); String realCode = (String) session.getAttribute("verify_code"); // 判断验证码是否为空或错误 if (userInputCode == null || realCode == null || !userInputCode.equalsIgnoreCase(realCode)) { req.setAttribute("msg", "❌ 验证码错误,请重新输入!"); req.getRequestDispatcher("/generate.jsp").forward(req, resp); return; } // ✅ 验证码正确,清除 session 中的验证码(防止二次提交) session.removeAttribute("verify_code"); // 模拟调用 Z-Image 模型进行推理(真实场景中会调用 Python 或 API) System.out.println("正在使用 Z-Image 生成图像..."); System.out.println("提示词:" + prompt); // 模拟耗时操作(比如GPU推理) try { Thread.sleep(2000); // 假设生成耗时2秒 } catch (InterruptedException ignored) {} // 成功返回 req.setAttribute("msg", "✅ 图像已生成成功!请查看本地输出目录。"); req.getRequestDispatcher("/generate.jsp").forward(req, resp); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } }

注意这一行:

session.removeAttribute("verify_code");

正是这一步实现了“一次性使用”的效果。只要验证码被成功验证一次,它就从 Session 中消失了。下次哪怕拿着同样的值来提交,也会因为realCode == null而失败。


设计要点总结

环节关键实现
验证码生成使用ValidateCode类动态绘制图像
存储方式放入HttpSession,绑定当前用户会话
安全传输每次刷新更换内容,配合前端防缓存策略
提交验证比对用户输入与 Session 中的原始值
防重复核心验证成功后立即删除 Session 中的验证码

这套机制简单却极其有效。它不依赖复杂的框架,也不需要数据库支持,仅靠 HTTP 会话就能完成防重放攻击。


生产环境增强建议

虽然上述方案已经可以应对大多数场景,但在实际部署中还可以进一步加固:

  1. 设置验证码有效期
    在 Session 中同时记录验证码生成时间,超过 60 秒自动作废,防止长期占用内存。

  2. 增加 IP 请求频率限制
    对同一 IP 地址单位时间内请求/vcode/Generate的次数进行监控,异常行为可临时封禁。

  3. 引入 Token 双重校验机制
    除了验证码,还可额外添加 CSRF Token,防止跨站伪造请求。

  4. 异步化处理高耗时任务
    不要让主线程阻塞等待 GPU 推理完成。应将任务推入消息队列(如 RabbitMQ、Kafka),由工作进程异步处理,提升系统吞吐能力。

  5. 前后端分离架构下的替代方案
    如果采用 Vue/React + Spring Boot 架构,可以用 UUID Token 替代验证码:
    - 页面加载时请求一个 token;
    - 提交时携带 token;
    - 后端用 Redis 缓存 token 并设置过期时间;
    - 成功后删除 token。

这种方式更适合无图形界面的 API 接口防护。


写在最后

在这个追求极致用户体验的时代,我们常常忽略了服务器的承受能力。但作为开发者,不能让用户的行为自由放纵。

通过一个小小的验证码机制,我们不仅能防止恶意刷接口,还能显著降低因网络延迟引发的误操作风险。尤其对于 AI 推理这类资源密集型应用,这种保护几乎是必需的。

记住一句话:

🛡️ 用户体验很重要,但服务器不该为用户的“多点几次”买单。

只要我们在关键操作前加上一道简单的验证防线,就能极大提升系统的健壮性和可用性。

如果你也在玩 Z-Image-ComfyUI 或类似的文生图项目,欢迎留言交流部署经验和优化技巧!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/10 14:34:21

锐龙3 3100/3300X首发性能实测:游戏逆袭

VibeThinker-1.5B-APP&#xff1a;小参数模型的推理逆袭之路 在AI大模型动辄千亿参数、训练成本破千万美元的今天&#xff0c;一个仅15亿参数、总花费不到8000美元的轻量级模型&#xff0c;却在数学与算法推理领域掀起波澜——它就是微博开源的 VibeThinker-1.5B-APP。 这不禁…

作者头像 李华
网站建设 2026/2/12 23:24:11

从Vector RAG到GraphRAG:大模型知识库的进化之路与ApeRAG实战指南

文章探讨了RAG技术发展趋势&#xff0c;指出传统Vector RAG面临信息碎片化和逻辑断层等局限&#xff0c;而GraphRAG通过知识图谱实现结构化思维和多跳推理。针对GraphRAG工程复杂度高的问题&#xff0c;ApeRAG作为生产级解决方案&#xff0c;通过混合索引机制、自动化图谱构建和…

作者头像 李华
网站建设 2026/2/14 9:34:02

怕被 AI 取代?留学生快冲这些 “AI+” 复合型岗位

今年关注北美科技行业新闻&#xff0c;总有一种让人“冰火两重天”的割裂感。 一边是狂热的加码&#xff1a;英伟达豪掷1000亿美元押注Open AI&#xff0c;要建能容纳400-500万块GPU的超级数据中心。从这些算力设备的部署运维&#xff0c;到下一代大模型的训练研发&#xff0c;…

作者头像 李华
网站建设 2026/2/13 22:08:55

从Pipeline 到对话:DataFlow-Agent 如何重构数据准备工程

这几年&#xff0c;大模型能力跃迁&#xff1a;它们能写代码、能回答问题、能规划步骤&#xff0c;甚至能代替我们做一些思考。 模型越来越聪明&#xff0c; 但只要把事情落到“数据”上&#xff0c;一切又回到了原点&#xff1a; 数据必须先被连接 数据必须被清洗 数据必须…

作者头像 李华
网站建设 2026/2/13 2:35:34

UTF-8编码解析与字符对照

IndexTTS 2.0&#xff1a;从文本编码到情感可控语音合成 你有没有遇到过这样的情况&#xff1a;精心写好的配音脚本&#xff0c;导入语音合成工具后&#xff0c;某个字突然读成了奇怪的音调&#xff1f;或者想让角色“愤怒地喊出一句台词”&#xff0c;结果生成的声音平淡如水&…

作者头像 李华
网站建设 2026/2/13 20:16:37

算法题 具有所有最深节点的最小子树

865. 具有所有最深节点的最小子树 问题描述 给定一个二叉树的根节点 root&#xff0c;返回包含所有最深节点的最小子树的根节点。 最深节点&#xff1a;距离根节点最远的叶子节点 最小子树&#xff1a;满足条件的子树中节点数最少的那个&#xff08;如果多个子树包含所有最深节…

作者头像 李华