简单文字验证码&&人机验证【Java】

发布于:2025-04-01 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、代码引用

首先,如果你想直接用,可以直接用下面这个类。

可以调用CaptchaGenerator类中的captchaCreateImage方法,其方法参数列表为(int width, int height, int captchaLength, String[] returnCaptcha, int degree),方法返回验证码图像

width -----------------------文字验证码图片的宽度

height-----------------------文字验证码图片的高度

captchaLength-----------文字验证码的长度

returnCaptcha-----------返回的文字验证码

degree---------------------干扰的程度(1-5,不在范围默认为5)

程度展示

 代码引用处

import java.awt.*; // 导入 AWT 图形库
import java.awt.geom.AffineTransform; // 导入用于执行几何变换的类
import java.awt.image.BufferedImage; // 导入用于处理图像的类
import java.util.Random; // 导入随机数生成器类
import java.util.concurrent.ThreadLocalRandom;

public class CaptchaGenerator {
    // 创建随机数生成器
    private final Random random = ThreadLocalRandom.current();

    // 创建验证码的方法
    private String createCaptcha(int length) {
        // 定义可选字符池(不包含容易混淆的字符0和O等)
        String charPool = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
        StringBuilder result = new StringBuilder(); // 用于构建验证码字符串

        // 随机选择字符生成验证码
        for (int i = 0; i < length; i++) {
            result.append(charPool.charAt(random.nextInt(charPool.length()))); // 从字符池中随机选择字符
        }
        return result.toString(); // 返回生成的验证码字符串
    }

    // 创建颜色的方法,生成指定范围内随机颜色
    private Color createColor(int min, int max) {
        int r = min + random.nextInt(max - min+1); // 随机生成红色分量
        int g = min + random.nextInt(max - min+1); // 随机生成绿色分量
        int b = min + random.nextInt(max - min+1); // 随机生成蓝色分量
        return new Color(r, g, b); // 创建并返回颜色对象
    }

    // 添加干扰元素的方法
    private void addInterference(Graphics2D g, int degree, int width, int height) {
        // 确保干扰元素的数量在0到5之间
        degree = (degree <= 0 || degree > 5) ? 5 : degree;

        // 根据度数生成干扰元素
        for (int i = 0; i < degree * 20; i++) {
            int x = random.nextInt(width); // 随机生成x坐标
            int y = random.nextInt(height); // 随机生成y坐标

            // 随机选择干扰元素的颜色
            Color color = (random.nextBoolean()) ? createColor(0, 255) : ((random.nextBoolean()) ? Color.WHITE : Color.BLACK);
            g.setColor(color); // 设置画笔颜色

            // 随机选择干扰元素的类型并画出
            switch (random.nextInt(3)) {
                case 0 -> g.fillOval(x, y, random.nextInt(3) + 1, random.nextInt(3) + 1); // 画圆点
                case 1 -> {
                    int change = random.nextInt(3); // 随机变化值
                    // 画出线条构成的随机图形
                    g.drawLine(x, y, x + change, y + change);
                    g.drawLine(x + change, y + change, x + 2 * change, y);
                    g.drawLine(x, y, x + change, y - change);
                    g.drawLine(x + change, y - change, x + 2 * change, y);
                }
                case 2 -> g.drawLine(x, y, x + random.nextInt(5) + 1, y + random.nextInt(5) + 1); // 画线
            }
        }

        // 生成更多随机干扰线
        for (int i = 0; i < 5 * degree; i++) {
            Color color = (random.nextBoolean()) ? createColor(0, 255) : ((random.nextBoolean()) ? Color.WHITE : Color.BLACK);
            g.setColor(color);
            // 随机生成干扰线的起止点
            g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height));
        }
    }

    // 创建验证码图像的方法
    private BufferedImage createImage(int width, int height, int captchaLength, String[] returnCaptcha, int degree) {

        // 创建新图像
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics(); // 获取图形上下文
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 启用抗锯齿

        // 创建背景色
        Color backgroundColor = createColor(0, 255);
        g.setColor(backgroundColor);
        g.fillRect(0, 0, width, height); // 填充背景

        // 生成验证码
        String captcha = createCaptcha(captchaLength);
        returnCaptcha[0] = captcha; // 将生成的验证码存入数组

        // 设置字体大小和干扰详细参数
        int fontSize = (int) (height * 0.5);
        AffineTransform at = new AffineTransform(); // 创建变换对象
        at.shear(random.nextDouble() * 0.4 - 0.2, random.nextDouble() * 0.4 - 0.2); // 随机倾斜变换

        Font font = new Font(Font.SANS_SERIF, Font.BOLD, fontSize); // 创建字体对象
        font = font.deriveFont(at); // 生成倾斜字体

        addInterference(g, degree, width, height); // 添加干扰元素

        Color fontColor, prevFontColor = null; // 字体颜色和前一个字体颜色
        int fontX, fontY, fontWidth, changeX; // 不同的坐标和宽度
        FontMetrics fontMetrics = g.getFontMetrics(); // 字体度量
        fontWidth = fontMetrics.stringWidth(captcha) + (captchaLength - 1) * (int) (width * 0.05); // 计算验证码的宽度
        fontX = (width - fontWidth) / 2; // 计算x坐标以居中对齐
        fontY = (height - (fontMetrics.getAscent() + fontMetrics.getDescent())) / 2 + fontMetrics.getAscent(); // 计算y坐标

        double tempX = fontX; // 保存当前x坐标

        // 逐个绘制验证码字符
        for (int i = 0; i < captchaLength; i++) {
            // 根据背景色的亮度生成对比度较强的字体颜色
            fontColor = (backgroundColor.getRed() > 180) ? createColor(0, 160) : createColor(200, 255);
            int maxAttempts = 10,count=0;//设置最大循环数,避免死循环
            // 确保字体颜色与前一个字体颜色不相近
            while (prevFontColor != null&&count++<maxAttempts) {
                double brightness = (backgroundColor.getRed() * 299 + backgroundColor.getBlue() * 114 + backgroundColor.getGreen() * 587) / 1000.0;
                fontColor = (brightness > 128) ? createColor(0, 128) : createColor(128, 255); // 确定字体颜色
                int dr = fontColor.getBlue() - prevFontColor.getBlue();
                int dg = fontColor.getGreen() - prevFontColor.getGreen();
                int db = fontColor.getRed() - prevFontColor.getRed();
                prevFontColor = fontColor; // 更新前一个颜色
                // 如果颜色差异大于亮度则退出循环
                if (Math.sqrt(dr * dr + dg * dg + db * db) > brightness) break;
            }

            prevFontColor = fontColor; // 更新前一个颜色
            g.setFont(font); // 设置当前字体
            g.setColor(fontColor); // 设置当前字体颜色

            // 随机旋转角度
            int RotationAngle = random.nextInt(60) - 30;
            g.rotate(Math.toRadians(RotationAngle), tempX, fontY); // 绕中心点旋转
            // 绘制字符
            g.drawString(String.valueOf(captcha.charAt(i)), (int) tempX, fontY);
            g.rotate(-Math.toRadians(RotationAngle), tempX, fontY); // 逆旋转恢复状态

            changeX = fontMetrics.stringWidth(String.valueOf(captcha.charAt(i))); // 获取当前字符宽度
            tempX += (changeX + (int) (width * 0.05)); // 更新临时x坐标,为下一个字符准备空间
        }
        g.dispose(); // 释放图形上下文资源
        if(captchaLength <= 0|| returnCaptcha[0].isEmpty()) {
            throw new IllegalArgumentException("returnCaptcha array must be non-null and have at least one element");
        }
        return image; // 返回生成的图像
    }
    public BufferedImage captchaCreateImage(int width, int height, int captchaLength, String[] returnCaptcha, int degree){
        return createImage(width, height, captchaLength, returnCaptcha, degree);
    }
}

二、代码实现

1.生成验证码

由于0和O等字符容易混淆,因此需要去除这些字符。

private String createCaptcha(int length){
        String charPool = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < length; i++) {
            result.append(charPool.charAt(random.nextInt(charPool.length())));
        }
        return result.toString();
    }

2.生成图片前的准备

(1)创建一个指定宽度、高度和类型的BufferedImage对象

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

(2)获取Graphics2D对象,用于绘制图像,并启用抗锯齿

Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

(3)填充背景颜色

Color backgroundColor = createColor(0, 255); // 随机生成背景颜色
g.setColor(backgroundColor); // 设置背景颜色
g.fillRect(0, 0, width, height); // 填充背景色

(4)获取验证码

String captcha = createCaptcha(captchaLength);
returnCaptcha[0]=captcha;

3.图片添加干扰

总体预览
private void addInterference(Graphics2D g,int degree,int width,int height){
        degree=(degree <= 0||degree > 5)?5:degree;
        for (int i = 0; i < degree*20; i++) {
            int x=random.nextInt(width);
            int y=random.nextInt(height);
            Color color=(random.nextBoolean())?createColor(0,255):((random.nextBoolean())?Color.WHITE:Color.BLACK);
            g.setColor(color);
            switch (random.nextInt(3)){
                case 0-> g.fillOval(x,y, random.nextInt(3)+1,random.nextInt(3)+1);
                case 1-> {
                    int change=random.nextInt(3);
                    g.drawLine(x,y,x+change,y+change);
                    g.drawLine(x+change,y+change,x+2*change,y);
                    g.drawLine(x,y,x+change,y-change);
                    g.drawLine(x+change,y-change,x+2*change,y);
                }
                case 2->g.drawLine(x,y,x+random.nextInt(5)+1,y+random.nextInt(5)+1);
            }
        }
        for(int i=0;i<5*degree;i++){
            Color color=(random.nextBoolean())?createColor(0,255):((random.nextBoolean())?Color.WHITE:Color.BLACK);
            g.setColor(color);
            g.drawLine(random.nextInt(width),random.nextInt(height),random.nextInt(width),random.nextInt(height));
        }
    }

(1)确保程度合法

degree=(degree <= 0||degree > 5)?5:degree;

 (2)生成噪点

a.随机坐标
int x=random.nextInt(width);
int y=random.nextInt(height);
b.颜色设置

要么彩色,要么黑白

Color color=(random.nextBoolean())?createColor(0,255):((random.nextBoolean())?Color.WHITE:Color.BLACK);
g.setColor(color);
 c.噪点样式选择
总体预览
switch (random.nextInt(3)){
                case 0-> g.fillOval(x,y, random.nextInt(3)+1,random.nextInt(3)+1);
                case 1-> {
                    int change=random.nextInt(3);
                    g.drawLine(x,y,x+change,y+change);
                    g.drawLine(x+change,y+change,x+2*change,y);
                    g.drawLine(x,y,x+change,y-change);
                    g.drawLine(x+change,y-change,x+2*change,y);
                }
                case 2->g.drawLine(x,y,x+random.nextInt(5)+1,y+random.nextInt(5)+1);
            }
1.椭圆形
case 0-> g.fillOval(x,y, random.nextInt(3)+1,random.nextInt(3)+1);
 2.菱形
case 1-> {
                    int change=random.nextInt(3);
                    g.drawLine(x,y,x+change,y+change);
                    g.drawLine(x+change,y+change,x+2*change,y);
                    g.drawLine(x,y,x+change,y-change);
                    g.drawLine(x+change,y-change,x+2*change,y);
                }
3.随机短线段
case 2->g.drawLine(x,y,x+random.nextInt(5)+1,y+random.nextInt(5)+1);

4.验证码内容处理

(1)处理字体

总体预览
int fontSize=(int)(height*0.5);
AffineTransform at = new AffineTransform();
at.shear(random.nextDouble()*0.4-0.2,random.nextDouble()*0.4-0.2);
Font font=new Font("微软雅黑", Font.BOLD,fontSize);
font=font.deriveFont(at);
g.setFont(font);
a.设置字体大小
int fontSize=(int)(height*0.5);
b.创建AffineTransform对象,用于几何变换,将字体扭曲,程度为[-0.2,0.2]。
AffineTransform at = new AffineTransform();
at.shear(random.nextDouble()*0.4-0.2,random.nextDouble()*0.4-0.2);
c.设置字体样式,并应用扭曲
Font font=new Font("微软雅黑", Font.BOLD,fontSize);
font=font.deriveFont(at);

 (2)字符居中处理

总体预览
int fontX,fontY,fontWidth,changeX;
FontMetrics fontMetrics = g.getFontMetrics();
fontWidth=fontMetrics.stringWidth(captcha)+(captchaLength-1)*(int)(width*0.05);
fontX=(width-fontWidth)/2;
fontY=(height - (fontMetrics.getAscent() + fontMetrics.getDescent())) / 2 + fontMetrics.getAscent();
a.获取横坐标

fontMetrics.stringWidth(captcha)是所有的字符的总宽度

(captchaLength-1)*(int)(width*0.05)是字符之间的总间隔大小,其中(captchaLength-1)为间隔数,(width*0.05)为间隔大小。

fontWidth=fontMetrics.stringWidth(captcha)+(captchaLength-1)*(int)(width*0.05);
fontX=(width-fontWidth)/2;
b.获取纵坐标
FontMetrics fontMetrics = g.getFontMetrics();
fontY=(height - (fontMetrics.getAscent() + fontMetrics.getDescent())) / 2 + fontMetrics.getAscent();

fontMetrics.getAscent():基线到字体最高点的距离

fontMetrics.getDescent():基线到字体最低点的距离

怎么理解?

FontMetrics类提供了关键参数:

  • Ascent:基线到字体最高点的距离(如字母"h"的顶部)。

  • Descent:基线到字体最低点的距离(如字母"g"的尾部)。

  • Leading:行间距(通常较小)。

  • Height:总高度(Ascent + Descent + Leading)。

一、将文字想象成一个“盒子”

假设每个字符是一个矩形盒子,其高度由三部分组成:

  1. Ascent(上升) :基线(baseline)到盒子顶部的距离(如字母"h"的顶部)。

  2. Descent(下降) :基线到盒子底部的距离(如字母"g"的尾部)。

  3. Leading(行间距) :盒子下方预留的空白(通常很小,可暂时忽略)。 

二、Java绘图的“基线对齐”

当调用 g.drawString(text, x, y) 时:

  • 参数y是基线的位置,而非文字区域的顶部或中心。

  • 如果直接将画布中点(height/2)作为基线位置,文字会整体偏下,因为Ascent部分会向上延伸,而Descent部分会向下延伸。

画布顶部(y=50)
|        |
|        |
|      —————————— ← Ascent(y=35)
|        
|      —————————— ← 基线(y=30)
|        
|      —————————— ← 画布中心(y=25)
|        ▲
|        | 
|        ▼
|      —————————— ← Descent(y=15)
|        |
|        |
画布底部(y=0)

 (3)字符颜色处理

总体预览
fontColor=(backgroundColor.getRed()>180)?createColor(0,160):createColor(200,255);
            int maxAttempts = 10,count=0;
            while (prevFontColor!=null&&count++<maxAttempts) {
                double brightness=(backgroundColor.getRed()*299+backgroundColor.getBlue()*114+backgroundColor.getGreen()*587)/1000.0;
                fontColor=(brightness>128)?createColor(0,128):createColor(128,255);
                int dr=fontColor.getBlue()-prevFontColor.getBlue();
                int dg=fontColor.getGreen()-prevFontColor.getGreen();
                int db=fontColor.getRed()-prevFontColor.getRed();
                prevFontColor=fontColor;
                if(Math.sqrt(dr*dr+dg*dg+db*db)>brightness)break;
            }
            prevFontColor=fontColor;
            g.setColor(fontColor);
a.随机颜色

避免与背景颜色相近。

fontColor=(backgroundColor.getRed()>180)?createColor(0,160):createColor(200,255);
b.处理相邻颜色相近与对比度不明显问题
总体预览
while (prevFontColor!=null&&count++<maxAttempts) {
                double brightness=(backgroundColor.getRed()*299+backgroundColor.getBlue()*114+backgroundColor.getGreen()*587)/1000.0;
                fontColor=(brightness>128)?createColor(0,128):createColor(128,255);
                int dr=fontColor.getBlue()-prevFontColor.getBlue();
                int dg=fontColor.getGreen()-prevFontColor.getGreen();
                int db=fontColor.getRed()-prevFontColor.getRed();
                prevFontColor=fontColor;
                if(Math.sqrt(dr*dr+dg*dg+db*db)>brightness)break;
            }
            prevFontColor=fontColor;
1.颜色差异检测机制(欧氏距离)

颜色距离公式
使用欧氏距离公式计算颜色差异:

\Delta = \sqrt{(R_1-R_2)^2 + (G_1-G_2)^2 + (B_1-B_2)^2}

该公式能综合衡量RGB三个通道的差异,更符合人眼对颜色差异的感知

2.背景对比度优化(YIQ模型)
  • YIQ亮度模型
    使用公式:

    Y = 0.299R + 0.587G + 0.114B
    

    该模型更贴近人眼对亮度的敏感度(绿色权重最高,蓝色最低)。

  • 对比度标准

    • 若背景亮度Y > 128(亮色背景),选择深色字体(如黑色、深蓝)

    • Y ≤ 128(暗色背景),选择浅色字体(如白色、亮黄)

      综合效果

    • 抗机器识别

      • 颜色差异和色相跳跃破坏OCR工具的颜色聚类算法,使其难以分割相邻字符。

      • 边缘描边干扰轮廓识别算法

    • 人类可读性

      • 高对比度确保文字清晰,符合人眼对亮度、色相的敏感特性。

      • 随机旋转和扭曲保留验证码的防机器特性,但不会影响人类阅读。

 (4)字符旋转处理

总体预览
int RotationAngle=random.nextInt(60)-30;
g.rotate(Math.toRadians(RotationAngle),tempX,fontY);
g.drawString(String.valueOf(captcha.charAt(i)),(int)tempX,fontY);
g.rotate(-Math.toRadians(RotationAngle),tempX,fontY);
changeX=fontMetrics.stringWidth(String.valueOf(captcha.charAt(i)));
tempX+=(changeX+(int)(width*0.05));
a.随机角度[-30°,30°]
int RotationAngle=random.nextInt(60)-30;
b.旋转字符
1.旋转画布
g.rotate(Math.toRadians(RotationAngle),tempX,fontY);
  • g.rotate(double theta, double x, double y) 方法用于旋转当前的 Graphics 上下文,影响之后的绘图操作

    • Math.toRadians(RotationAngle):将角度从度转换为弧度,因为 Java 的 rotate 方法需要以弧度为单位的旋转角度。

    • tempX 和 fontY:这里指定了旋转中心的坐标。

      • tempX:是文本的当前 x 坐标,通常根据文本的长度动态计算,使得字符绘制在适当的位置。

      • fontY:是文本的 y 坐标,通常是一个固定值,确保文本在画布的某一高度。

通过这行代码,当前画布将围绕指定的 (tempX, fontY) 点旋转,旋转角度为 RotationAngle

2.绘制字符 
g.drawString(String.valueOf(captcha.charAt(i)),(int)tempX,fontY);
  • g.drawString(String str, int x, int y) 方法用于在画布上绘制字符串。

  • String.valueOf(captcha.charAt(i)):从名为 captcha 的字符串中获取索引为 i 的字符,并将其转换为字符串(通常可以直接使用 captcha.charAt(i))。

  • (int)tempX 和 fontY:这是绘制文本的坐标。

    • tempX:是在之前计算的 x 坐标,代表文本在水平方向上的位置。

    • fontY:是文本在垂直方向上的固定高度。

这行代码则在旋转后的画布上绘制当前字符。

3.恢复画布的旋转状态
g.rotate(-Math.toRadians(RotationAngle),tempX,fontY);
  • 这行代码用于撤销之前的旋转操作。

    • -Math.toRadians(RotationAngle):使用负的角度进行旋转,与之前的旋转相反。

    • tempX 和 fontY:依然是旋转中心的坐标。

这个步骤确保在绘制完当前字符后,画布的状态返回到原始位置,方便后续绘制其他字符时不会受到影响。

c.绘制字符位置处理
changeX=fontMetrics.stringWidth(String.valueOf(captcha.charAt(i)));
tempX+=(changeX+(int)(width*0.05));
1. 计算字符宽度
  • captcha.charAt(i):从 captcha 字符串中获取索引 i 处的字符。这是当前需要绘制的字符。

  • String.valueOf(...):将获取的字符转换为字符串。虽然在 Java 中,char 类型可以直接用于字符串操作,但使用 String.valueOf 方法能确保我们在需要时转换为字符串类型。

  • fontMetrics.stringWidth(...):这个方法是 FontMetrics 类中的一个方法,它可以返回指定字符串在当前字体下的宽度(以像素为单位)。也就是说,它计算出当前正在使用的字体绘制 String.valueOf(captcha.charAt(i)) 这一个字符所需要的宽度。

  • changeX:这是一个变量,用于存储当前字符的宽度。这将被用来调整下一个字符的绘制位置。

 2.更新绘制位置
  • tempX:这是一个变量,表示当前字符在绘图上下文中的 x 坐标。它记录了下一个字符应该绘制的 horizontal (水平方向) 位置。

  • changeX:上面计算获得的当前字符宽度。

  • (int)(width * 0.05):这部分代码用于在字符之间添加一定的间距。在这里,width 是整张图片的宽度,而 width * 0.05 表示取宽度的 5%。通过将结果转换为 int,确保我们将这个浮点值作为整数处理,适配绘图 API 对坐标的要求。

  • +=:这个操作符用于将当前的 tempX 值加上当前字符的宽度和额外的间距,然后更新 tempX 的值。这一步确保下一个字符绘制时不会重叠,而是位于上一个字符的右侧,并且留有一定的间隙。