Java生成验证码案例怎么写

wen java案例 2

Java生成验证码案例怎么写?从零到生产级的完整指南(附核心代码)

目录导读

  1. 为什么验证码是Java Web应用的“安全守门员”?
  2. 验证码生成的底层原理:图形绘制与随机算法
  3. 手把手实战:基于Java原生API的验证码案例(纯Servlet+ImageIO)
  4. 进阶优化:如何加入干扰线、噪点、扭曲与字符粘连
  5. 生产级方案:集成Redis验证码存储与Spring Boot接口
  6. 高频面试问答:验证码防刷、识别与性能瓶颈
  7. Java验证码的最佳实践路线图

为什么验证码是Java Web应用的“安全守门员”?

在Web安全攻防中,验证码(CAPTCHA)的核心作用是区分人类用户与自动化程序,Java后端开发中,常见场景包括:登录防暴力破解、注册防批量刷号、评论防垃圾灌水、支付接口防重放,一个设计良好的验证码案例,不仅要能生成图片,还需要考虑时效性、一次性、服务端校验等安全维度。

Java生成验证码案例怎么写

根据搜索引擎聚合的技术文章(如CSDN、Stack Overflow、Baeldung),大多数教程只教“画图”,却忽略“校验逻辑”,本文将从图形渲染服务端状态管理双维度,给你一套完整可落地的方案。


验证码生成的底层原理:图形绘制与随机算法

Java生成验证码的底层依赖以下核心类:

  • java.awt.BufferedImage:内存中的图像缓冲区,用于绘制像素。
  • java.awt.Graphics2D:提供绘图方法,如画字符串、画线、填充椭圆。
  • javax.imageio.ImageIO:将内存图像编码为JPEG/PNG输出流。

随机算法上,常见策略是从字符池(排除易混淆的0/O、1/l)随机选取N个字符,生成随机颜色、随机旋转角度、随机字体,为了防OCR(光学字符识别),还需加入:

  • 干扰线(随机坐标连线)
  • 噪点(随机像素点)
  • 波浪形扭曲(通过Graphics2D的shear或transform方法)

关键点:验证码图片只负责“生成”,而校验值必须存放在服务端Session或Redis中,严禁存入Cookie(可被篡改)。


手把手实战:基于Java原生API的验证码案例(纯Servlet+ImageIO)

这是搜索热度最高的基础案例,适合初学者理解完整流程,以下代码经过“去伪存真”优化,删除了网上常见的无效逻辑(如未设置响应头导致的图片乱码)。

1 创建验证码生成工具类 CaptchaUtil.java

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.security.SecureRandom;
public class CaptchaUtil {
    private static final int WIDTH = 120;
    private static final int HEIGHT = 40;
    private static final char[] CHARS = {
        'A','B','C','D','E','F','H','J','K','M','N','P','Q','R','S','T','U','V','W','X','Y',
        '2','3','4','5','6','7','8','9'
    };
    private static final SecureRandom RANDOM = new SecureRandom();
    public static String generate(HttpServletResponse response) throws IOException {
        // 1. 创建空白图像
        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = image.createGraphics();
        // 2. 填充背景色(浅灰色渐变效果)
        g2d.setColor(new Color(245, 245, 245));
        g2d.fillRect(0, 0, WIDTH, HEIGHT);
        // 3. 绘制随机干扰线(5条)
        g2d.setStroke(new BasicStroke(1.5f));
        for (int i = 0; i < 5; i++) {
            g2d.setColor(new Color(RANDOM.nextInt(200), RANDOM.nextInt(200), RANDOM.nextInt(200)));
            g2d.drawLine(RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT),
                    RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT));
        }
        // 4. 生成4位随机字符并绘制
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < 4; i++) {
            char c = CHARS[RANDOM.nextInt(CHARS.length)];
            code.append(c);
            // 随机字体、大小、颜色、旋转
            Font font = new Font("Arial", Font.BOLD | Font.ITALIC, 24 + RANDOM.nextInt(8));
            g2d.setFont(font);
            g2d.setColor(new Color(30 + RANDOM.nextInt(100), 30 + RANDOM.nextInt(100), 30 + RANDOM.nextInt(100)));
            // 轻微旋转(-30度到30度)
            double angle = (RANDOM.nextInt(60) - 30) * Math.PI / 180;
            g2d.rotate(angle, 20 + i * 25, HEIGHT / 2);
            g2d.drawString(String.valueOf(c), 15 + i * 25, 28 + RANDOM.nextInt(6));
            g2d.rotate(-angle, 20 + i * 25, HEIGHT / 2);
        }
        // 5. 添加噪点(随机小矩形)
        for (int i = 0; i < 30; i++) {
            g2d.setColor(new Color(RANDOM.nextInt(255), RANDOM.nextInt(255), RANDOM.nextInt(255)));
            g2d.fillRect(RANDOM.nextInt(WIDTH), RANDOM.nextInt(HEIGHT), 1, 2);
        }
        // 6. 释放资源并输出
        g2d.dispose();
        response.setContentType("image/jpeg");
        response.setHeader("Cache-Control", "no-store");
        ImageIO.write(image, "jpeg", response.getOutputStream());
        return code.toString();
    }
}

2 编写Servlet控制器 CaptchaServlet.java

import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/captcha")
public class CaptchaServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String code = CaptchaUtil.generate(resp);
        // 将验证码存入Session(注意:生产环境建议用Redis)
        req.getSession().setAttribute("captcha_code", code);
        req.getSession().setMaxInactiveInterval(60); // 60秒过期
    }
}

3 前端调用与校验逻辑

<!-- html片段 -->
<img src="/captcha" onclick="this.src='/captcha?t='+new Date().getTime()"/>
<!-- 提交时校验 -->
<p>输入验证码:</p>
<input name="captchaInput">
<button onclick="submitForm()">提交</button>
<script>
function submitForm() {
    const input = document.querySelector('input[name="captchaInput"]').value;
    fetch('/verifyCaptcha', {
        method: 'POST',
        body: new URLSearchParams({captchaInput: input})
    }).then(res => res.text()).then(msg => alert(msg));
}
</script>

校验Servlet片段

String sessionCode = (String) req.getSession().getAttribute("captcha_code");
String inputCode = req.getParameter("captchaInput");
if(sessionCode != null && sessionCode.equalsIgnoreCase(inputCode)) {
    // 成功逻辑(立即移除,防复用)
    req.getSession().removeAttribute("captcha_code");
}

注意:很多网帖在equalsIgnoreCase后不删除Session属性,这是严重漏洞,会导致验证码可复用,被脚本无限提交。


进阶优化:如何加入干扰线、噪点、扭曲与字符粘连

若你的网站面临较高攻击风险,基础案例不够安全,以下优化点来自Baeldung和GitHub开源项目的精华:

优化手段 实现方式 防识别效果
字符粘连 使用等宽字体(如Courier New)加上字符间重叠绘制 防止基于字符分割的OCR
贝塞尔曲线干扰 g2d.drawPath绘制2-3条波浪曲线 显著增加图形复杂度
背景纹理 填充随机渐变或细密网格 干扰二值化处理
变色渐变 每个字符单独或连续渐变色 使颜色分割失效
弹性扭曲 使用AffineTransform对图像进行整体扭曲(如正弦波变换) 极难还原

示例代码:添加贝塞尔曲线

// 在绘制字符之前,添加两条随机曲线
for (int i = 0; i < 2; i++) {
    int x1 = RANDOM.nextInt(WIDTH/2);
    int y1 = RANDOM.nextInt(HEIGHT);
    int x2 = WIDTH/2 + RANDOM.nextInt(WIDTH/2);
    int y2 = RANDOM.nextInt(HEIGHT);
    int ctrlx1 = RANDOM.nextInt(WIDTH);
    int ctrlx2 = RANDOM.nextInt(WIDTH);
    g2d.setColor(new Color(100 + RANDOM.nextInt(100), 100 + RANDOM.nextInt(100), 100 + RANDOM.nextInt(100)));
    g2d.drawPath(new Path2D.Float() {{
        moveTo(x1, y1);
        curveTo(ctrlx1, y1 + 20, ctrlx2, y2 - 20, x2, y2);
    }});
}

生产级方案:集成Redis验证码存储与Spring Boot接口

在分布式或微服务架构中,Session共享是痛点,推荐使用Redis存储验证码,这是当前企业级项目的标准做法。

1 核心设计

  • Key格式captcha:uuid:用户标识或客户端ID
  • Value:验证码字符串
  • TTL:60~120秒
  • 验证后立即删除DEL key(原子操作)

2 Spring Boot实现片段

@RestController
public class CaptchaController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/captcha/prod")
    public void getCaptcha(@RequestParam String clientId, HttpServletResponse response) throws IOException {
        String code = CaptchaUtil.generate(response);
        stringRedisTemplate.opsForValue().set(
            "captcha:" + clientId, code, 120, TimeUnit.SECONDS
        );
    }
    @PostMapping("/verify")
    public Result verify(@RequestBody VerifyDTO dto) {
        String redisKey = "captcha:" + dto.getClientId();
        String saved = stringRedisTemplate.opsForValue().get(redisKey);
        if (saved != null && saved.equalsIgnoreCase(dto.getInput())) {
            stringRedisTemplate.delete(redisKey); // 一次性使用
            return Result.success("验证通过");
        }
        return Result.error("验证码错误或已失效");
    }
}

3 为何不使用JWT或前端存储?

尽管有人尝试把验证码放入JWT,但这是不安全的,因为JWT可被解析且无法主动注销,Redis方案不仅支持过期,还能配合防刷限流(如同一clientId每分钟最多请求3次验证码)。


高频面试问答:验证码防刷、识别与性能瓶颈

Q1:如何防止验证码被OCR自动识别?

答案:多管齐下——使用干扰线+噪点+扭曲;限制字符长度为4~6;定时更换字符池;增加逻辑计算验证码(如“3+5=?”),高级方案可用行为验证码(如滑块/点选字),但Java后端生成逻辑更复杂。

Q2:验证码图片加载慢或占用内存大?

答案:用JPEG代替PNG可减小体积但牺牲清晰度;控制图片尺寸在150x50以内;使用BufferedImage.TYPE_INT_RGB压缩率更高,若并发巨大,可预生成一批图片存入内存缓存(如Caffeine),但需注意随机性。

Q3:为什么我设置了Cache-Control: no-store,图片还是被缓存?

答案:需同时设置Pragma: no-cache(兼容HTTP/1.0)和Expires: 0,部分浏览器还要求加上Last-Modified头,但最有效的是在URL后拼接随机参数(如时间戳t)。

Q4:验证码在集群环境下Session失效怎么办?

答案:采用第5章节的Redis方案,或使用Spring Session以Redis存储Session,避免使用自带的本机Session。

Q5:高频接口(如登录)验证码被秒刷?

答案:结合计数限流——同一IP或用户标识每60秒只生成1次验证码;若不通过则禁止刷新,同时可以通过Thread.sleep(50)微延迟降低自动化脚本速度。


Java验证码的最佳实践路线图

从“能画出来”到“生产可用”,Java验证码案例的关键演进路径:

  1. 基础版:原生Servlet + ImageIO + Session(适合学习)
  2. 进阶版:增加复杂干扰效果(干扰线、噪点、扭曲、粘连)
  3. 工程版:Spring Boot + Redis + 一次性校验 + 限流
  4. 商业版:接入第三方验证码服务(如极验、腾讯验证码)或内部行为识别引擎

最终建议:如果你的项目不是高安全等级,使用本文第3章源码即可;但涉及支付、后台管理、用户中心,请务必升级到第5章的Redis方案,并加上防刷逻辑。验证码只是第一道防线,还需配合IP黑名单、失败次数锁定等策略。


基于Java 8+技术栈,适配Spring Boot 2.x/3.x,代码可直接复制运行调试。)*

抱歉,评论功能暂时关闭!