资讯详情

EasyCaptcha:Spring Boot图形验证码集成与生产实践

📅 2026/10/16 0:54:08 | 华诺云谱 👁 阅读
EasyCaptcha:Spring Boot图形验证码集成与生产实践
1. 从零到一为什么我最终选择了EasyCaptcha先说结论在Java生态里做验证码EasyCaptcha是目前我用过最省心的开源库没有之一。如果你正在被各种验证码框架的配置折磨或者刚接手一个需要图形验证码的老项目这篇内容应该能帮你省下至少一个下午的排查时间。事情是这样的前阵子接手了一个旧的管理系统登录模块用的是那种上古时代的手写验证码——BufferedImageGraphics2D手动画干扰线代码冗长不说每次部署环境一变字号渲染就出问题。客户的诉求很朴素验证码清楚一点、安全一点、别动不动崩。我第一反应是找一个现成的库替换掉。当时考虑过几个方向Google 的 reCAPTCHA 需要外网交互不适合纯内网部署Kaptcha 曾经很流行但项目维护状态一般而且依赖较重JCaptcha 配置起来略啰嗦。EasyCaptcha 是 com.github.whvcse 开源的一个轻量项目核心思路非常直接一行代码生成支持 GIF 动图、中文、算术、英数混合四种验证码类型。它不是重框架没有繁琐的接口不强制绑定 Spring MVC随手就能集成。我实测下来的感受是EasyCaptcha 最大的优势是心智负担小。核心只有两个类SpecCaptcha生成验证码和CaptchaUtilSpring MVC 下的工具类代码量控制在千行级别出了问题直接看源码就能定位。对于不想在验证码这种边缘功能上投入太多维护精力的团队来说这个选择几乎是零思考成本的。如果你也是以下情况之一EasyCaptcha 会非常适合你项目是传统 Spring Boot 单体应用需要快速给登录/注册模块加一道图形验证码不想引入 Redis 做验证码存储就想用 Session 兜底需要 GIF 动态验证码来对抗截图识别又不希望引入太重的验证码服务想自己控制验证码长度、字体、颜色、干扰方式但不想从头写绘图代码。下面我会按集成步骤 → 原理解析 → 踩坑排查 → 安全加固的顺序把从依赖引入到生产部署的全过程拆开讲。全程基于 Spring Boot 2.7 环境JDK 8 以上均适用。2. 三分钟跑通Maven依赖、接口编写与前端接入2.1 引入依赖与核心参数速查EasyCaptcha 在 Maven 中央仓库的坐标如下目前稳定版本在 1.6.2 左右dependency groupIdcom.github.whvcse/groupId artifactIdeasy-captcha/artifactId version1.6.2/version /dependency引入这一个依赖就够了它内部不传递其他第三方库非常干净。这一点对喜欢依赖最小化的团队是很大的加分项不会出现依赖冲突的连锁反应。核心类就两个SpecCaptcha最常用的图形验证码类支持长度、宽高、字体、干扰线、噪点等设置可输出png或gif格式。CaptchaUtilSpring MVC 场景下的快捷工具类内部封装了output方法直接往HttpServletResponse里写图片流。SpecCaptcha最常用构造函数是public SpecCaptcha(int width, int height, int len, char[] chars)四个参数分别是图片宽度、图片高度、验证码字符个数、可选的字符集。如果最后一个参数不传默认使用内置的英数混合字符集去掉了容易混淆的 0、O、1、l、I 等字符。常用的配置项速查表方法作用示例setFont设置字体可用系统字体名captcha.setFont(new Font(楷体, Font.PLAIN, 32))setCharType切换类型TYPE_DEFAULT英数、TYPE_ONLY_NUMBER纯数字、TYPE_ONLY_CHAR纯字母、TYPE_ONLY_UPPER大写字母、TYPE_ONLY_LOWER小写字母、TYPE_NUM_AND_CHAR数字字母captcha.setCharType(Captcha.TYPE_ONLY_NUMBER)setLen设置验证码长度captcha.setLen(4)out输出图片到输出流captcha.out(outputStream)text获取验证码文本值用于存 SessionString code captcha.text()toBase64生成 base64 字符串方便前端直接使用String base64 captcha.toBase64()2.2 后端接口先写一个最小可用版本在 Spring Boot 里一个最基础的验证码接口长这样RestController public class CaptchaController { GetMapping(/captcha) public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException { // 设置响应头告诉浏览器这是一个图片流 response.setContentType(image/png); response.setHeader(Cache-Control, no-store, no-cache); // 生成 130x48 的 4 位验证码 SpecCaptcha captcha new SpecCaptcha(130, 48, 4); captcha.setCharType(Captcha.TYPE_DEFAULT); // 把验证码文本存到 Session request.getSession().setAttribute(captcha, captcha.text().toLowerCase()); // 输出图片流 captcha.out(response.getOutputStream()); } }这段代码有几点值得注意缓存头必须设置。Cache-Control如果不加有些浏览器会缓存图片导致刷新验证码时看起来没变化。no-store比no-cache更彻底直接禁止存储。Session 里存的是小写。captcha.text()返回的是图片上显示的原文可能大小写混合。统一转小写存储校验时也统一转小写可以避免Aa这类大小写误判。不要在这里做任何复杂的业务逻辑。验证码接口会被高频请求尤其是刷新按钮被用户狂点时保持接口轻量。2.3 前端接入img标签 点击刷新 表单提交前端部分比我预想的还简单因为 EasyCaptcha 的接口就是直接吐图片流所以一个img标签就够了img idcaptchaImg src/captcha alt验证码 title点击刷新 / script // 点击图片刷新验证码 document.getElementById(captchaImg).addEventListener(click, function () { // 加一个时间戳防止浏览器使用缓存的旧图片 this.src /captcha?t Date.now(); }); /script这个Date.now()是必须的。有一种情况是即使后端设置了Cache-Control某些旧版浏览器或者反向代理层仍然可能缓存图片。加了时间戳查询参数URL 每次都不一样彻底杜绝缓存问题。表单提交时把用户输入的验证码一起提交到后端后端从 Session 取出之前存的验证码进行比对。这一步看起来简单但就是在这里我后面踩了一个不小的坑这个放到第 4 节详细说。2.4 一种更省事的写法用 CaptchaUtil 替代手动装配如果你不想每次都在 Controller 里写setContentType、setHeader、out这几板斧可以直接用CaptchaUtilRestController public class CaptchaController { GetMapping(/captcha) public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException { SpecCaptcha captcha new SpecCaptcha(130, 48, 4); request.getSession().setAttribute(captcha, captcha.text().toLowerCase()); CaptchaUtil.out(response, captcha); } }CaptchaUtil.out内部已经帮你处理了 ContentType、缓存头、输出流关闭这些琐碎操作。源码很短我翻过里面核心逻辑就是public static void out(HttpServletResponse response, Captcha captcha) throws IOException { response.setContentType(image/png); response.setHeader(Cache-Control, no-store, no-cache); captcha.out(response.getOutputStream()); }因为是直接封装了标准Captcha接口所以只要你实现Captcha接口也可以把自己的验证码实现传给CaptchaUtil灵活性足够。2.5 验证码校验写一个简单的拦截逻辑后端校验的代码很简单但要注意一点校验通过后必须立刻从 Session 中移除验证码防止同一验证码被重放。这是一种基本的安全 hygiene 习惯。PostMapping(/login) public Result login(RequestParam String username, RequestParam String password, RequestParam String captchaInput, HttpSession session) { // 1. 从 Session 获取验证码 Object sessionCaptcha session.getAttribute(captcha); // 2. 不管结果如何先用掉这个验证码 session.removeAttribute(captcha); // 3. 校验 if (sessionCaptcha null || !sessionCaptcha.toString().equalsIgnoreCase(captchaInput.trim())) { return Result.error(验证码错误或已失效); } // 4. 验证码通过继续用户名密码校验 // ... }这一段我特意把removeAttribute放在校验之前而不是之后。原因是如果先校验再移除遇到并发请求时理论上存在重放窗口。虽然实际攻击场景里要精确利用这个窗口不太容易但养成验证码一次性使用的习惯没有坏处。3. 不只是画图EasyCaptcha 的核心绘制原理与选型分析3.1 验证码的本质噪声对抗识别很多人觉得验证码就是个画图工具随便画画就行。其实验证码设计的核心是加噪声 → 增加机器识别成本同时保持人类识别友好度的平衡。EasyCaptcha 的底层绘制逻辑也不复杂核心思路可以类比为先写正文再洒墨点再画干扰线。第一步在图片背景上画验证码字符每个字符位置、颜色、角度都做随机处理。第二步随机画一些干扰线贝塞尔曲线让字符轮廓和背景内容粘连增加机器分割字符的难度。第三步撒噪点类似旧电视的雪花点进一步干扰像素级识别。有意思的是EasyCaptcha 的实现不是直接用随机数硬画而是维护了一个随机字符串 随机字体 随机颜色的组合。每生成一次验证码颜色和字符间距都不同所以同一个文本在不同时刻生成的图片长得完全不一样这给暴力枚举和历史样本复用增加了成本。3.2 GIF 与 PNG 的选择逻辑EasyCaptcha 支持两种输出格式PNG 静态图和 GIF 动态图。首次接触的人可能会直接选 GIF觉得动态的一定更安全。但实际使用中要注意GIF 的生成成本比 PNG 高不少因为它会生成多帧图片每帧都是一次完整的绘制过程。我做过一个简单的压测对比500 次循环生成输出格式单次平均耗时图片体积安全性PNG4位约 3ms2-4 KB基础对抗GIF4位5帧约 8-12ms15-30 KB动态轨迹对抗截图在低并发登录场景下PNG 的 3ms 和 GIF 的 10ms 差距感知不到。但如果你的登录接口会被高频请求比如营销活动页、抢购场景GIF 的 CPU 开销会被放大需要额外关注。我的建议是普通后台管理系统用 PNG 就够面对公网用户、对安全性要求较高的场景再上 GIF。GIF 的动态效果会让一些基础的 OCR 工具直接失效因为每一帧的噪点位置和干扰线都在变静态截图无法还原完整背景。3.3 为什么 EasyCaptcha 比手写 BufferedImage 靠谱前面说过我接手的老项目就是手写验证码。手写的最大问题是不可控字体在不同操作系统上的渲染差异。开发环境是 Windows部署到 CentOS 后字体变方块干扰线多了人眼识别率骤降干扰线少了机器识别轻松代码散落在项目各处没有一个统一抽象改一个参数要全局搜。EasyCaptcha 把这些都封装好了。它内部使用Font.createFont读取内置的.ttf字体文件不依赖系统字体从根源上解决了字体漂移问题。这一点对部署在 Docker 容器里的应用尤其重要因为容器基础镜像里往往没有中文字体你手写一个带中文的验证码在容器里大概率直接渲染成方块。3.4 源码里的一个小彩蛋干扰线是贝塞尔曲线我读源码时注意到EasyCaptcha 的干扰线不是简单的直线而是贝塞尔曲线。它的实现是随机选取几个控制点然后沿着曲线路径画点。这样做的好处是干扰线的走势更加自然不容易被简单的像素连通域分析算法剥离。如果你需要写一份技术方案汇报可以把基于贝塞尔曲线的动态干扰作为 EasyCaptcha 的一个技术亮点写进去比含糊地说带干扰线专业得多。4. 生产环境实测我从踩坑到排坑的完整链路这一节是全文的精华。EasyCaptcha 虽然简单但真正部署上线后那些看起来不多想就没事的细节会一个个冒出来。我把自己踩过的坑和排查过程完整记录下来希望你能直接跳过。4.1 坑一Session 中存的是谁的值—— 大小写与小写化的连锁反应现象用户输入验证码时偶尔会提示验证码错误而且看起来输入的字符和图片一模一样代码审查了好几遍都找不到问题。排查过程第一步我先在 Controller 里加了日志打印captcha.text()的实际值和用户提交值log.info(session captcha: {}, session.getAttribute(captcha)); log.info(user input: {}, captchaInput);登录几次后发现问题规律当图片上出现大写字母时用户如果照着大写输入反而会校验失败。因为我在生成接口里做了一步captcha.text().toLowerCase()存入 Session 的是小写而校验时用的是captchaInput.trim()原样比对没有转小写。用户输入ASession 里是aequals就返回 false。根因我为了规避大小写混淆问题在存入 Session 时做了小写化却在校验端忘了做同样的小写化。典型的只做了一半。修复校验时统一转小写boolean isValid sessionCaptcha.toString().equalsIgnoreCase(captchaInput.trim());或者干脆从一开始就用equalsIgnoreCase不去改 Session 里的值。我最终选择的是校验端用equalsIgnoreCase生成端保持原值存储这样以后想改成严格大小写敏感也方便。4.2 坑二分布式部署下 Session 失效 —— 验证码到底存哪里现象系统部署了多个实例前端请求经过负载均衡转发。用户第一次请求验证码落在实例 A提交登录时请求落在实例 B而 Session 是单机内存的结果验证码永远校验失败。排查过程这个问题在开发环境根本不会出现因为只有一个实例。到测试环境我用 Nginx 做了负载均衡后立刻爆出。当时第一反应是EasyCaptcha 是不是有 bug翻了源码发现它根本不管理验证码存储只是生成图片和返回文本。存储方案完全由使用者决定。排查结论不是 EasyCaptcha 的问题而是我的架构没适配分布式。验证码是有状态数据必须放在多个实例共享的存储中。修复方案引入 Redis 存储验证码以 UUID 作为 key验证码文本作为 value设置 5 分钟过期时间。前端拿到验证码图片时同时拿到一个captchaId提交时带上这个 ID后端用 ID 去 Redis 查询。GetMapping(/captcha) public Result captcha(HttpServletResponse response) { SpecCaptcha captcha new SpecCaptcha(130, 48, 4); String captchaId UUID.randomUUID().toString().replaceAll(-, ); redisTemplate.opsForValue().set(captcha: captchaId, captcha.text().toLowerCase(), 5, TimeUnit.MINUTES); response.setContentType(image/png); response.setHeader(Cache-Control, no-store, no-cache); captcha.out(response.getOutputStream()); return Result.success(captchaId); }等等这里有个接口返回值问题out已经写图片流了再返回 JSON 会导致响应体被污染。我实际采用的方案是把captchaId放在response.addHeader(Captcha-Id, captchaId)前端从响应头里取。或者更干净的方法用 Base64 返回图片内容让接口统一返回 JSON。GetMapping(/captcha) public Result captcha() { SpecCaptcha captcha new SpecCaptcha(130, 48, 4); String captchaId UUID.randomUUID().toString().replaceAll(-, ); redisTemplate.opsForValue().set(captcha: captchaId, captcha.text().toLowerCase(), 5, TimeUnit.MINUTES); return Result.success(Map.of( captchaId, captchaId, captchaImage, captcha.toBase64() )); }前端拿到 base64 后直接赋给img srcdata:image/png;base64,...避免了图片流和 JSON 交叉的问题。这种方式在前后端分离项目里更常见。如果你不想额外生成一个 UUID也可以直接用 Session ID 作为 key但注意 Session ID 在某些极端配置下可能不唯一自己权衡。4.3 坑三GIF 验证码在高并发下的性能毛刺现象某次活动页上线瞬时注册量暴增监控发现登录接口的 P99 耗时明显上涨。通过链路追踪定位到验证码生成这一段耗时翻倍。排查过程我第一反应是 GIF 的问题因为当时用的是动态验证码。用 JFR 抓了一轮热点方法发现SpecCaptcha.gifEncodes相关方法占用 CPU 显著。原因不难理解GIF 每帧都要重新绘制图形、计算调色板、压缩编码5 帧的工作量约等于 5 张静态图。验证思路我写了一段灰度代码按用户分组一部分流量走 PNG一部分走 GIF对比耗时分位数。结果 PNG 的 P99 在 8ms 以内GIF 的 P99 到了 25ms 以上。修复验证码属于安全控制手段不是业务核心不需要在所有场景都上 GIF。最终调整策略为登录接口用 PNG注册接口用 GIF并且给 GIF 的验证码设置较短的过期时间3分钟。在安全性和性能之间取了一个平衡点。4.4 坑四验证码接口被刷——图片本身没坏坏的是接口现象运维反馈某段时间验证码接口请求量异常高一分钟几十万次大量请求不是来自登录页面而是直接打接口地址。排查过程验证码接口本身是公开的不校验身份任何人都可以直接 HTTP GET 获取图片。攻击者可以批量请求生成验证码既不登录也不提交纯粹消耗服务器 CPU 和带宽。这种半攻击性质的行为验证码自身防不住。修复对验证码接口做基础限流。按 IP 维度每分钟最多请求 60 次超过直接返回 429。这个策略在前端 Nginx 层做最合适limit_req_zone $binary_remote_addr zonecaptcha:10m rate60r/m; location /captcha { limit_req zonecaptcha burst20 nodelay; proxy_pass http://backend; }如果不想引入 Nginx 配置也可以用后端拦截器配合本地缓存的计数器做简易限流。核心思想是验证码接口必须防刷否则就失去了拦截机器人的意义反而变成攻击者的算力消耗器。4.5 坑五图片能显示但很模糊部分用户反馈看不清现象有用户反馈验证码字体笔画太细在手机上放大后依然模糊。排查过程我用手机浏览器试了试发现同样是 130x48 的图片PC 上看着很清楚手机上因为屏幕密度高DPR 2 甚至 3图片被拉伸后边缘发虚。修复思路生成图片时按 DPR 倍数放大。前端检测window.devicePixelRatio如果是 2请求/captcha?dpr2后端生成260x96的图片。但要注意验证码文本长度和字号也要等比例放大否则只是画布变大了字符还是那么小更看不清。EasyCaptcha 的setFont允许动态设置字号所以我根据 dpr 参数动态调整int dpr Integer.parseInt(request.getParameter(dpr) ! null ? request.getParameter(dpr) : 1); SpecCaptcha captcha new SpecCaptcha(130 * dpr, 48 * dpr, 4); captcha.setFont(new Font(Arial, Font.PLAIN, 32 * dpr));这样手机端拿到的是物理像素更高的图片显示清晰度有明显改善。5. 安全加固进阶如何把验证码从摆设变成防线很多开发者的验证码停留在防小白水平实际上连最简单的 OCR 都防不住。这里分享几个我在实践中确认有效的加固手段。5.1 随机算术验证码对抗 OCR 的简单招数EasyCaptcha 支持算术验证码即生成类似3 5 ?的图片用户输出 8。这个方式的优势是OCR 识别结果里没有数字的语义组合背景单纯 OCR 只能得到 3 5很难自动输出正确答案。用法很简单ArithmeticCaptcha captcha new ArithmeticCaptcha(130, 48); captcha.setLen(2); // 几位数运算 String text captcha.text(); // 结果是 35? String answer captcha.getArithmeticString(); // 这个是 35这里有一个非常隐蔽的坑ArithmeticCaptcha.text()返回的不是答案我当初想当然地以为text()是计算结果8结果存进 Session 的是表达式35?。正确的做法是看它的源码里面getArithmeticString()返回表达式而答案需要通过一个简单的解析器算出来。其实ArithmeticCaptcha内部有getResult()方法吗我翻版本的时候没有找到统一的答案获取方法需要自己写一个四则运算解析器或者限定只用加减法。我的建议是如果要上算术验证码就只做两位数加减法不要做乘除和负数。一是用户计算成本低二是解析器一分钟就能写出来。// 简单加减法解析 public static int calc(String expr) { // expr 形如 35 12-7 // 不需要引入第三方表达式库避免过度设计 }5.2 中文验证码常用字与生僻字的取舍EasyCaptcha 支持ChineseCaptcha图片上显示中文汉字。中文验证码在对抗纯字母 OCR 时有一定优势因为大多数英文 OCR 引擎不擅长识别汉字尤其是手写的、扭曲的汉字。但中文验证码有个用户体验问题生僻字用户根本不认识。我实测遇到龘这种字用户直接懵了。所以如果上中文验证码务必使用常用字表char[] chars 用你我他天地人大小上下左右一二三四五六七八九十.toCharArray(); ChineseCaptcha captcha new ChineseCaptcha(130, 48, 4, chars);注意ChineseCaptcha默认字库可能包含一些不常用汉字覆盖一下更稳妥。用户体验排序大约是纯数字 英数混合 算术 中文。安全强度排序则大致相反所以要根据业务风险等级做选择。后台管理系统这种用户群稳定的场景纯数字足够面向公网的注册接口建议算术或英数混合。5.3 两段式校验前后端都加一道闸门单纯靠后端验证码其实很多无效请求已经打到后端了。一种常见的加固思路是前端先校验验证码格式比如 4 位英数格式不对直接拦截不发请求后端再做一次完整校验。这样减少无效请求对后端接口的压力。前端那块可以在提交表单时用正则校验const captchaValue document.getElementById(captchaInput).value.trim(); if (!/^[a-zA-Z0-9]{4}$/.test(captchaValue)) { alert(请输入4位字符); return; }后端校验除了格式还要注意验证码的时效性。如果 Session 或者 Redis 中不存在这个验证码说明过期了不要只是简单提示验证码错误应该提示验证码已过期请刷新。大多数用户能理解而攻击者的自动化脚本往往不会处理刷新逻辑会一直卡在同样的问题上。5.4 验证码与登录频率限制组合拳验证码不是独立的它是防暴力破解链条的一环。我见过一些系统验证码做得很漂亮但登录接口没有任何频率限制攻击者第一个请求拿到验证码截图识别或者人工打码后面一万个请求只需要带同一个验证码重放……好吧这个例子有点极端但如果你的校验逻辑不是一次性使用就会存在重放窗口。推荐的做法是把验证码和 IP 维度频率限制叠加同一个 IP5 分钟内最多 10 次登录尝试第 5 次失败开始要求必须输入验证码第 10 次失败锁定该 IP 15 分钟。验证码承担的职责从每次都验证变成异常时重点拦截——这样用户体验更好安全强度反而不降。6. 高并发场景下的性能调优与扩展思考EasyCaptcha 单次生成耗时很低但如果你遇到极端流量还是有几个优化点可以尝试。6.1 验证码生成结果缓存空间换时间如果你的验证码类型固定比如都是 4 位英数 PNG可以考虑启动时预热一批验证码图片放入本地缓存如 Caffeine请求时直接从缓存里取一张同时记录它对应的文本值。这样把生成耗时从 3ms 降到 0.1ms 以内。注意缓存里的验证码需要标记状态被取走并绑定到用户 Session 后立即失效。这种方案适合验证码流量特别大又有少量分布式节点的场景。我实际测试下来预热 1000 张图片命中率可以达到 80% 以上因为每个验证码发出去后立即销毁缓存里的都是待下发的。这个方案有一个隐含风险如果本地缓存没有真正标记已被使用同一张图可能发放给多个用户验证码结果一致会让攻击者更容易批量破解。所以缓存方案里每张图片被取出时必须立刻生成唯一 ID 绑定到 Redis 并移除缓存条目不能复用。我在落地时用了ID 列表 状态标记的方式复杂度可控。6.2 线程池与阻塞out() 方法是否会阻塞captcha.out()实际上是一个 IO 操作写 OutputStream在高并发下如果一边生成图片一边写响应线程可能会在 IO 上等待。Spring Boot 默认的 Tomcat 线程池通常能扛住普通验证码请求但如果验证码接口跟其他重 IO 接口共用一个线程池验证码生成慢会拖累整个应用。我的建议是给验证码接口配置独立的线程池执行器或者干脆把它放到反应式栈里如果项目用 WebFlux。当然这属于极端优化绝大多数项目用不到。先做好前面说的防刷限流比盲目调线程池更有效。6.3 日志与监控验证码也要有可观测性别笑验证码接口虽然简单但它的监控指标能反映很多问题生成耗时如果突然升高可能有人在刷接口成功/失败校验比例如果失败率超过 30%说明有自动化脚本在盲试或者用户确实被难住了单 IP 请求量超过阈值自动告警。我在项目里用 micrometer 记录了captcha.generate.time和captcha.verify.count两个指标配合 Grafana 面板看趋势。一开始觉得小题大做直到线上真出现了一次刷接口事件监控图表直接报警才发现这钱花得值。7. 最终实践总结与后续扩展建议写到这里EasyCaptcha 基本已经说得比较透了。你如果现在准备在项目里集成我的建议是按照这个顺序来引入依赖写一个最简单的/captcha接口返回 PNG 图片流前端用 img 标签接住联调登录校验确认 Session 存取和一次性校验逻辑正确如果项目是多节点部署立刻把 Session 换成 Redis 存储给验证码接口加上限流上线后观察监控指标再决定要不要上 GIF、算术验证码或缓存预热。EasyCaptcha 本身只是一个生成工具它不负责存储、不负责校验、不负责防刷这些都需要你自己搭。但也正因为它的边界清晰你可以在它上面构建出完全符合自己业务形态的安全方案而不是被一个重框架牵着走。最后分享一个小技巧如果你要设计新的验证码样式别只调参数直接把Captcha接口实现了注入到CaptchaUtil.out()里EasyCaptcha 的整个输出链路就能复用。这比 fork 源码改绘制逻辑优雅得多。我现在手头这个项目的验证码模块已经稳定运行了大半年没有再遇到什么幺蛾子。你用的时候如果遇到其他奇怪的问题欢迎回来交流——毕竟验证码这玩意儿看起来简单实际细节多得很。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑