2026/1/15 23:09:22
网站建设
项目流程
长春模板建站代理,给视频做特效的网站,福建中海建设有限公司网站,青岛做网站建设多少钱使用验证码防止表单重复提交#xff1a;基于 JSP Servlet 的实战方案
在开发 Web 应用时#xff0c;你有没有遇到过用户疯狂点击“提交”按钮导致服务雪崩的情况#xff1f;尤其是在涉及高计算成本的操作中#xff0c;比如 AI 图像生成、订单支付或注册流程#xff0c;这…使用验证码防止表单重复提交基于 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 languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% % String path request.getContextPath(); String basePath request.getScheme() :// request.getServerName() : request.getServerPort() path /; % !DOCTYPE html html head base href%basePath% meta http-equivContent-Type contenttext/html; charsetUTF-8 titleZ-Image AI绘图/title script typetext/javascript window.onload function () { var img document.getElementById(vcode_img); img.onclick function () { this.src vcode?date new Date().getTime(); }; } /script /head body h2 stylecolor:#333; 使用 Z-Image 生成你的专属图像/h2 form actionGenerate methodpost 提示词input typetext nameprompt placeholder例如一只戴着墨镜的猫在月球上冲浪 stylewidth:300px/brbr 验证码input typetext namevcode maxlength4 stylewidth:60px/nbsp; img srcvcode idvcode_img alt点击刷新 stylecursor:pointer;brbr input typesubmit value立即生成 / /form !-- 显示结果 -- % String msg (String) request.getAttribute(msg); if (msg ! null) { % div stylemargin-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 会话就能完成防重放攻击。生产环境增强建议虽然上述方案已经可以应对大多数场景但在实际部署中还可以进一步加固设置验证码有效期在 Session 中同时记录验证码生成时间超过 60 秒自动作废防止长期占用内存。增加 IP 请求频率限制对同一 IP 地址单位时间内请求/vcode或/Generate的次数进行监控异常行为可临时封禁。引入 Token 双重校验机制除了验证码还可额外添加 CSRF Token防止跨站伪造请求。异步化处理高耗时任务不要让主线程阻塞等待 GPU 推理完成。应将任务推入消息队列如 RabbitMQ、Kafka由工作进程异步处理提升系统吞吐能力。前后端分离架构下的替代方案如果采用 Vue/React Spring Boot 架构可以用 UUID Token 替代验证码- 页面加载时请求一个 token- 提交时携带 token- 后端用 Redis 缓存 token 并设置过期时间- 成功后删除 token。这种方式更适合无图形界面的 API 接口防护。写在最后在这个追求极致用户体验的时代我们常常忽略了服务器的承受能力。但作为开发者不能让用户的行为自由放纵。通过一个小小的验证码机制我们不仅能防止恶意刷接口还能显著降低因网络延迟引发的误操作风险。尤其对于 AI 推理这类资源密集型应用这种保护几乎是必需的。记住一句话️ 用户体验很重要但服务器不该为用户的“多点几次”买单。只要我们在关键操作前加上一道简单的验证防线就能极大提升系统的健壮性和可用性。如果你也在玩 Z-Image-ComfyUI 或类似的文生图项目欢迎留言交流部署经验和优化技巧