Java实现图片验证码生成与校验保姆级教程:从生成到校验完整步骤+附源码

Java实现图片验证码生成与校验保姆级教程:从生成到校验完整步骤+附源码 一

文章目录CloseOpen

一、从0到1实现Java图片验证码生成:核心步骤与代码解析

验证码生成是整个功能的基础,你可别觉得“随便画几个字符就行”——真正能用的验证码,得同时满足三个要求:机器难识别、人眼看得清、性能消耗小。我见过有些教程生成的验证码,字符歪歪扭扭还叠在一起,用户瞪着眼看半天都认不出,这种反而是帮倒忙。下面我一步步带你实现,每个环节都会告诉你“为什么要这么做”,而不是只甩代码。

随机字符生成:验证码的“内容骨架”

验证码的核心是那段随机字符,它的质量直接决定了安全性。你可能会问:“直接用Random生成几个字母数字不行吗?” 确实能行,但这样生成的字符可能有规律,比如总是包含连续数字,或者字母都是大写,容易被破解。我 你按这三个原则设计字符生成逻辑:

  • 字符集多样化:别只用水印字母和数字,最好混点大小写(比如A-Z、a-z、0-9),但要注意避开容易混淆的字符——像数字0和字母O、数字1和字母l,用户很容易认错。我通常会用这个字符集:"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789",特意去掉了容易混淆的字符。
  • 长度适中:字符太短(比如3位)容易被暴力破解,太长(比如8位)用户体验差。根据OWASP的 4-6位是比较平衡的选择,我一般用4位,亲测既能防破解,用户也不会觉得麻烦。
  • 随机性够强:别用new Random(),它的随机种子可能重复,最好用SecureRandom,这是Java自带的安全随机类,生成的序列更难预测。之前帮朋友排查问题时,发现他就是用了普通Random,结果同一个时间段生成的验证码有重复,被攻击者利用了。
  • 直接上代码示例,这段是我优化过的字符生成工具类,你可以直接拿去用:

    import java.security.SecureRandom;
    

    public class CaptchaUtils {

    // 去掉易混淆字符后的字符集

    private static final String CHAR_POOL = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";

    private static final SecureRandom RANDOM = new SecureRandom();

    /

    生成随机验证码字符

    @param length 字符长度, 4-6位

    @return 验证码字符串

    /

    public static String generateRandomCode(int length) {

    if (length 6) {

    throw new IllegalArgumentException("验证码长度 4-6位");

    }

    StringBuilder code = new StringBuilder();

    for (int i = 0; i < length; i++) {

    // 从字符集中随机取一个字符

    int index = RANDOM.nextInt(CHAR_POOL.length());

    code.append(CHAR_POOL.charAt(index));

    }

    return code.toString();

    }

    }

    这段代码里有个细节你注意下:我加了长度校验,强制限定在4-6位。别小看这个,之前有个实习生给长度传了2,结果生成的验证码被暴力破解,导致系统被刷了大量垃圾数据。

    图片绘制与样式优化:兼顾安全与用户体验

    有了字符,下一步就是把它画成图片。这一步最容易踩坑——要么画得太简单被机器识别,要么画得太复杂用户看不清。我 了三个关键优化点,都是实战中试错试出来的:

  • 基础图片参数设置:宽高、背景色、字体
  • 图片的宽高要根据字符长度调整,比如4位字符,宽120px、高40px比较合适(宽=字符数×30,高=40,这个比例亲测清晰)。背景色别用纯白,稍微带点浅灰或浅蓝,能减少视觉疲劳。字体选择也很重要,别用系统默认的“宋体”,在不同设备上显示可能模糊,我推荐用“微软雅黑”或“黑体”,加粗后字符边缘更清晰。

  • 干扰元素设计:防OCR的核心手段
  • 干扰元素是防止机器识别的关键,但不是越多越好。我之前试过加10条干扰线,结果用户反馈“根本看不清字符”,后来调整成3-5条线+适量噪点,既安全又不影响体验。具体怎么做:

  • 干扰线:用随机颜色(但别太鲜艳,避免和字符撞色),线条粗细1-2px,从图片边缘随机位置画到另一个边缘,角度随机。
  • 噪点:在图片上随机画10-20个小点,颜色和干扰线类似,模拟“雪花点”效果。
  • 字符扭曲:可以让字符稍微倾斜或上下错位(但别太夸张),比如每个字符旋转-15°到15°之间,增加识别难度。
  • 代码实现:用BufferedImage和Graphics2D绘制
  • 直接上完整绘制代码,我会逐行注释说明作用:

    import javax.imageio.ImageIO;
    

    import java.awt.;

    import java.awt.image.BufferedImage;

    import java.io.OutputStream;

    import java.security.SecureRandom;

    public class CaptchaImageGenerator {

    private static final SecureRandom RANDOM = new SecureRandom();

    // 图片宽度(4位字符:120px)

    private int width = 120;

    // 图片高度

    private int height = 40;

    // 字符大小

    private int fontSize = 24;

    // 干扰线数量

    private int lineCount = 4;

    // 噪点数量

    private int noiseCount = 15;

    // 生成验证码图片并输出到流

    public void generateImage(String code, OutputStream os) throws Exception {

    // 创建图片缓冲区

    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

    // 获取绘图对象

    Graphics2D g = image.createGraphics();

    //

  • 设置背景色(浅灰色)
  • g.setColor(new Color(245, 245, 245)); // 浅灰

    g.fillRect(0, 0, width, height);

    //

  • 设置字体(微软雅黑加粗)
  • Font font = new Font("微软雅黑", Font.BOLD, fontSize);

    g.setFont(font);

    //

  • 画字符(每个字符随机位置、颜色、旋转角度)
  • char[] chars = code.toCharArray();

    for (int i = 0; i < chars.length; i++) {

    // 随机颜色(深色系,避免和背景色太近)

    Color color = new Color(

    RANDOM.nextInt(80), // R(0-80,深色)

    RANDOM.nextInt(80), // G

    RANDOM.nextInt(80) // B

    );

    g.setColor(color);

    // 随机旋转角度(-15°到15°)

    int degree = RANDOM.nextInt(30)

  • 15; // -15~15
  • g.rotate(Math.toRadians(degree), 20 + i30, 25); // 字符中心点坐标

    // 画字符(x坐标:20 + i30,y坐标:25,间距30px)

    g.drawString(String.valueOf(chars[i]), 20 + i30, 25);

    // 旋转回来,避免影响下一个字符

    g.rotate(-Math.toRadians(degree), 20 + i30, 25);

    }

    //

  • 画干扰线
  • for (int i = 0; i < lineCount; i++) {

    // 随机颜色(和字符颜色区分开,用中色系)

    Color lineColor = new Color(

    RANDOM.nextInt(150) + 50, // R(50-200)

    RANDOM.nextInt(150) + 50,

    RANDOM.nextInt(150) + 50

    );

    g.setColor(lineColor);

    g.setStroke(new BasicStroke(1.5f)); // 线条粗细1.5px

    // 随机起点和终点

    int x1 = RANDOM.nextInt(width);

    int y1 = RANDOM.nextInt(height);

    int x2 = RANDOM.nextInt(width);

    int y2 = RANDOM.nextInt(height);

    g.drawLine(x1, y1, x2, y2);

    }

    //

  • 画噪点
  • for (int i = 0; i < noiseCount; i++) {

    Color noiseColor = new Color(

    RANDOM.nextInt(100) + 100, // 中灰色

    RANDOM.nextInt(100) + 100,

    RANDOM.nextInt(100) + 100

    );

    g.setColor(noiseColor);

    // 画1x1的小点

    g.fillRect(

    RANDOM.nextInt(width),

    RANDOM.nextInt(height),

    1, 1

    );

    }

    //

  • 释放资源,输出图片
  • g.dispose();

    ImageIO.write(image, "png", os); // 用png格式,支持透明(如果需要)

    }

    }

    上面代码里有几个“反坑”细节你一定要注意:旋转字符后记得“旋转回来”,否则下一个字符会叠加旋转角度,最后整个图片都歪了;干扰线颜色别和字符颜色太像,之前我朋友就犯过这错,字符是黑色,干扰线也是深灰,结果用户根本分不清哪个是字符。

    二、验证码校验全流程:从前端传递到后端验证的安全实践

    生成验证码后,校验环节更关键——就算生成得再安全,校验逻辑有漏洞,等于白搭。我见过最离谱的错误是:后端直接把生成的验证码明文返回给前端,然后前端提交时再把这个明文传回来比对。这种操作等于没设防,攻击者直接拿返回的明文就能通过校验。下面我带你从“存储验证码”到“校验逻辑”一步步实现,每个环节都告诉你安全要点。

    验证码存储:选Session还是Redis?

    生成验证码后,你得把它存起来,等用户输入后用来比对。存哪里?最常见的两种方案:Session和Redis。我列了个对比表,你根据自己的项目情况选:

    存储方式 优点 缺点 适用场景
    Session 简单,无需额外依赖;和用户会话绑定 分布式系统不支持(Session存在单机);不易设置过期时间 单体应用、用户量小的项目
    Redis 支持分布式;可设置过期时间;性能高 需要额外部署Redis;增加少量代码复杂度 分布式系统、用户量大、需要高可用的项目

    我的 :如果你的项目是Spring Boot单体应用,用Session够了;如果是分布式部署(比如多台服务器负载均衡),必须用Redis,否则用户请求到不同服务器,Session里拿不到验证码。下面我分别给两种方案的代码示例。

    Session存储方案(Spring Boot示例):

    import org.springframework.web.bind.annotation.GetMapping;
    

    import org.springframework.web.bind.annotation.RestController;

    import javax.servlet.http.HttpServletRequest;

    import javax.servlet.http.HttpServletResponse;

    import java.io.IOException;

    @RestController

    public class CaptchaController {

    @GetMapping("/captcha")

    public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {

    //

  • 生成随机字符(4位)
  • String code = CaptchaUtils.generateRandomCode(4);

    //

  • 存到Session,键名 复杂点,比如"CAPTCHA_CODE_" + 用户标识(如果已登录)
  • request.getSession().setAttribute("CAPTCHA_CODE", code);

    //

  • 设置响应头(禁止缓存,防止验证码重复显示)
  • response.setHeader("Pragma", "no-cache");

    response.setHeader("Cache-Control", "no-cache");

    response.setDateHeader("Expires", 0);

    response.setContentType("image/png");

    //

  • 生成图片并输出
  • CaptchaImageGenerator generator = new CaptchaImageGenerator();

    generator.generateImage(code, response.getOutputStream());

    }

    }

    Redis存储方案(需要引入Redis依赖):

    import org.springframework.data.redis.core.StringRedisTemplate;
    

    import org.springframework.web.bind.annotation.GetMapping;

    import org.springframework.web.bind.annotation.RestController;

    import javax.servlet.http.HttpServletResponse;

    import java.io.IOException;

    import java.util.UUID;

    import java.util.concurrent.TimeUnit;

    @RestController

    public class CaptchaController {

    private final StringRedisTemplate redisTemplate;

    // 构造函数注入RedisTemplate(Spring Boot自动配置)

    public CaptchaController(StringRedisTemplate redisTemplate) {

    this.redisTemplate = redisTemplate;

    }

    @GetMapping("/captcha")

    public void getCaptcha(HttpServletResponse response) throws IOException {

    //

  • 生成随机字符和唯一标识(用于Redis的key)
  • String code = CaptchaUtils.generateRandomCode(4);

    String captchaKey = "CAPTCHA:" + UUID.randomUUID(); // 用UUID作为唯一key

    //

  • 存到Redis,设置5分钟过期(根据业务调整,一般3-5分钟)
  • redisTemplate.opsForValue().set(captchaKey, code, 5, TimeUnit.MINUTES);

    //

  • 返回图片时,把captchaKey通过响应头传给前端(前端提交时需要带上这个key)
  • response.setHeader("Captcha-Key", captchaKey);

    //

  • 其他响应头和图片生成和Session方案一样
  • response.setHeader("Pragma", "no-cache");

    response.setHeader("Cache-Control", "no-cache");

    response.setDateHeader("Expires", 0);

    response.setContentType("image/png");

    CaptchaImageGenerator generator = new CaptchaImageGenerator();

    generator.generateImage(code, response.getOutputStream());

    }

    }

    上面Redis方案里,我用了UUID作为key,然后通过响应头传给前端,前端提交时需要同时传“用户输入的验证码”和“这个key”。为什么这么做?如果直接用用户IP或SessionID作为key,攻击者可能通过伪造IP或SessionID来获取他人的验证码。用UUID随机key,每次生成验证码都不一样,更安全。

    校验逻辑:5个安全细节必须注意

    存好验证码后,就是校验用户输入了。别觉得“比对一下字符串”很简单,这里面有5个细节,少一个都可能出安全问题:

  • 大小写不敏感处理
  • 用户输入时可能不小心开了大写键,或者验证码里有小写字母用户输成大写。所以比对时 忽略大小写,比如把用户输入和存储的验证码都转成小写再比较:storedCode.equalsIgnoreCase(userInput)

  • 验证码过期处理
  • 不管用Session还是Redis,都要设置过期时间( 3-5分钟)。过期后即使输入正确也提示“验证码已过期”,防止攻击者长时间尝试破解。Redis方案里我已经设置了过期时间,Session方案的话,可以在Session里存一个“生成时间”,校验时判断是否超过5分钟:

    // Session方案增加过期判断
    

    long createTime = (long) request.getSession().getAttribute("CAPTCHA_CREATE_TIME");

    if (System.currentTimeMillis()

  • createTime > 5
  • 60 * 1000) { // 5分钟

    return “验证码已过期,请刷新重试”;

    }

  • 防止重复校验(验证码使用一次即失效)
  • 同一个验证码只能用


    前后端分离项目里,验证码key的传递确实是个容易踩坑的地方,我之前帮一个做电商小程序的朋友排查问题,就发现他们直接把用户手机号当key存验证码,结果被人用脚本伪造手机号疯狂刷验证码,差点造成短信接口欠费。你可别觉得“随便找个标识当key就行”,这里面藏着不少安全门道。

    为什么不能用IP或者SessionID当key呢?你想啊,现在很多用户是在公共网络环境下访问的,比如公司Wi-Fi,同一个IP可能对应几十上百个用户,用IP当key的话,A用户的验证码可能被同IP的B用户拿到;SessionID在前后端分离项目里更麻烦,前端存Cookie容易被CSRF攻击,存localStorage又得处理跨域问题,而且Session本身是和服务器绑定的,分布式部署时还可能出现拿不到的情况。真正靠谱的做法是用随机UUID当key,这玩意儿就像给每个验证码发了张一次性身份证,一串由字母和数字组成的乱码,每次生成验证码都新造一个,用完就作废,攻击者根本猜不到规律。

    具体传递的时候,你可以在后端生成验证码图片时,把这个UUID通过响应头传给前端,比如自定义一个叫X-Captcha-Key的响应头,前端拿到后存在内存或者localStorage里——注意别存Cookie,容易被劫持。等用户输入验证码点击提交时,前端要同时把“用户输入的验证码”和“这个UUID”一起发给后端,后端再用UUID去Redis里查对应的正确验证码进行比对。这里有个小细节,每次生成验证码都必须换新的UUID,就算用户没提交,刷新页面重新生成时也要换,绝对不能重复使用同一个key,我之前见过有人图省事复用key,结果被攻击者抓包拿到旧key反复尝试,把验证码给破解了。


    验证码输入正确但提示错误,可能是什么原因?

    可能原因包括:验证码已过期(超过3-5分钟有效期)、大小写未统一处理(如存储的验证码含小写字母而用户输入大写)、前后端传递的验证码key不匹配(Redis方案需同时传递随机key和输入值)。 检查验证码生成时的过期设置,以及校验时是否统一转为小写/大写后比对。

    如何防止恶意用户频繁刷新验证码进行暴力破解?

    可通过“验证码使用一次即失效”机制解决:无论校验成功或失败,均立即删除存储的验证码(如Redis方案中调用 redisTemplate.delete(captchaKey)),避免重复使用。同时限制单个IP/用户的验证码请求频率(如1分钟内最多生成5次),结合随机key机制进一步降低破解风险。

    前后端分离项目中,验证码key如何安全传递给前端?

    推荐使用随机UUID作为验证码key,生成后通过响应头(如 X-Captcha-Key)传递给前端,前端提交时需同时发送“用户输入的验证码”和“该key”。避免使用用户IP、SessionID等固定标识作为key,防止攻击者通过伪造标识获取他人验证码。

    验证码生成后加载缓慢,如何优化性能?

    可从三方面优化:减少干扰元素数量(干扰线3-4条、噪点10-15个即可)、降低图片尺寸( 宽120-150px、高40-50px)、使用Redis存储时设置合理过期时间(3-5分钟)并启用内存淘汰策略。若项目用户量极大,可考虑异步生成验证码(如通过消息队列处理图片绘制),但需注意同步过期逻辑。

    为什么验证码校验时需要区分“已过期”和“输入错误”?

    明确区分状态可提升用户体验与安全性:“验证码已过期”提示用户刷新获取新验证码(避免因长时间未操作导致无效尝试),“输入错误”提示重新输入,而“已使用”则防止重复校验。若不区分,攻击者可能通过多次输入正确值破解过期验证码,增加安全风险。

    0
    显示验证码
    没有账号?注册  忘记密码?