做一个简单的Web图形验证码

先来说说为什么会有验证码这么个反人类的玩意 每次输入验证码 365°的都是错误 然后重新输入 随着时间的推移 验证码这玩意 越来越高级越来越难 某班的验证码还是汉字 简直了 但是呢 深处互联网时代 这个验证码可以说说 必不可少的 怎么说呢 举个简单的 🌰
现在有一个系统需要登录登出 那注册也是必不可少的吧 如果现在注册没有验证码 那注册账号 就显得简单多了 那么问题来了 注册是一个向服务器发起的请求 有人抓取注册的请求协议 直接可以写一个批量的注册机 一秒就可以注册上千个账号 是不是问题就来了

互联行为的注册、登录、发帖、领优惠券、投票等等应用场景,都有被机器刷造成各类损失的风险,如果不对各类机器垃圾的行为加以防范,灌水内容、垃圾注册、恶意登录、刷票、撞库、活动作弊、垃圾广告、爬虫、羊毛党等用户行为一旦发生,将对产品自身发展、用户体验造成极大的影响。

好了 废话不讲 直接上干货

为了防止机器识别 OCR之类 需要加点难度

  1. 噪点 干扰线
  2. 字体大小
  3. 字体风格
  4. 字体颜色
  5. 字体位置
  6. 字体旋转角度

我们从上面6个方面入手

先做干扰线 就是画线条
public void drawInterferingLind(Graphics g, int level) {
        for (int i = 0; i < level; i++) {
            int x = mRandom.nextInt(mImgWidth);
            int y = mRandom.nextInt(mImgHeight);
            int x1 = mRandom.nextInt(mImgWidth);
            int y1 = mRandom.nextInt(mImgHeight);
            g.setColor(getRandomColor());
            g.drawLine(x, y, x1, y1);
        }
    }
随机字体大小 和 风格
private Font getRandomFontStyle() {
        //随机字体名称
        String fontName = mFont[mRandom.nextInt(mFont.length)];
        //随机字体大小 初始值为最小26 最大36
        int fontSize = mRandom.nextInt(10) + 26;
        //给出2/3的概率设为字体加粗
        int fontStyle = mRandom.nextInt(3) == 1 ? Font.PLAIN : Font.BOLD;
        Font font = new Font(fontName, fontStyle, fontSize);
        return font;
    }
随机字体颜色

由于我们的背景是白色的 如果是完全随机验证的画 会一半的概率发现 验证码 看不起 难搞哦 所以对于文本 我们需要深色系列的字体 。RBG颜色控制在115以内 干扰线的话 就没那么讲究了 直接255 随机色值

/**
     * 获取随机颜色 深色系列 用于文本显示
     *
     * @return
     */
    private Color getDarkColor() {
        return new Color(mRandom.nextInt(115), mRandom.nextInt(115), mRandom.nextInt(115));
    }
综合上面 附加 字体位置 和 旋转角度

这里需要提的几个点

  • 刚刚开始为了实现字体有一定夹角 我用了 Graphics 强转为2DGraphics 这样就可以达到旋转的效果了 但是发现他旋转的是整个画布 我们绘制是一个字符 一个字符绘制的 每次旋转多少 绘制完一个字符 就需要 逆转回去 不然就会出现 字符转没了 4个字符 需要旋转8次 还很麻烦 后来查了下 发现 font.deriveFont可以设置 AffineTransform 这就很理想了
  • 由于字符是平分 底图画板的宽度的 也就是X轴的位置差不多固定了 我有加了点随机偏移值 但是 如果带上旋转的话 会发现 首位2个字符 可能会消失一半 原因是 在旋转时 如果首字符逆时针旋转了 他就跑外面去了 底图就那么大 这里2种解决办法
  • 1.一开始在绘制的时候 就限制掉绘制的首位字符的X轴 让他离边距产生点距离 保证旋转的时候不跑出去
  • 2.让首位字符不产生夹角 也就是不旋转 那也就没有因为夹角过大消失的问题了 这里我选了第二种办法解决
/**
     * 绘制验证码文本
     *
     * @param g
     */
    private void drawCaptchaText(Graphics g, int textLength) {
        //获取文本
        String randomChars = getRandomChars(textLength);
        mCaptchaText = randomChars;
        //设置文本颜色
        g.setColor(getDarkColor());
        for (int i = 0; i < textLength; i++) {
            //设置随机字体风格
            Font font = getRandomFontStyle();
            /*
            首尾2个字符不进行旋转 如果旋转 可能会只出现半个字符 另一半旋转到边缘去了
             */
            if (!(i == 0 || i == textLength - 1)) {
                //设置随机旋转角度  在 -45~45°区间
                Double angle = Math.toRadians(mRandom.nextInt(90) - 45);
                //创建一个2D可旋转字体对象
                AffineTransform affineTransform = new AffineTransform();
                //设置旋转角度
                affineTransform.rotate(angle);
                //替换Font
                font = font.deriveFont(affineTransform);
            }
            g.setFont(font);
            //设置x的坐标 将布局空间分配个字符串加上一点随机偏移量
            int x = mImgWidth / textLength * i + mRandom.nextInt(mImgWidth / 4 / 2);
            //设置y的坐标 在绘制文本以文本底变为y的坐标 设置一个最低高度 以免文本太靠上消失
            int y = mRandom.nextInt(mImgHeight - font.getSize()) + font.getSize();
            g.drawString(String.valueOf(randomChars.charAt(i)), x, y);
        }
    }
最后附上效果

tesserocr 验证码去干扰线 去除验证码的粗干扰线_验证码

tesserocr 验证码去干扰线 去除验证码的粗干扰线_tesserocr 验证码去干扰线_02

tesserocr 验证码去干扰线 去除验证码的粗干扰线_tesserocr 验证码去干扰线_03


应该还可以 差不多了

总结

验证码还是很有用的 上面的代码也都很简单 问题可能就在于 随机夹角吧 其他应该都还可以 最后附上Captcha类

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * @author Bee
 */
public class Captcha {

    private Random mRandom = new Random();

    /**
     * 随机字体字体
     */
    private String[] mFont = {"宋体", "楷体", "黑体", "微软雅黑", "仿宋"};
    /**
     * 随机字符
     */
    private String mCodes = "qwertyuipasdfghjklxcvbnm1234567890QWERTYUPASDFGHJKLZXCVBNM";
    /**
     * 验证码文本
     */
    private String mCaptchaText = "";
    /**
     * 验证码图形宽度
     */
    private int mImgWidth = 0;
    /**
     * 验证码图形高度
     */
    private int mImgHeight = 0;

    public String getCaptchaText() {
        return mCaptchaText.toLowerCase();
    }

    public int getImgWidth() {
        return mImgWidth;
    }

    public void setImgWidth(int mImgWidth) {
        this.mImgWidth = mImgWidth;
    }

    public int getImgHeight() {
        return mImgHeight;
    }

    public void setImgHeight(int mImgHeight) {
        this.mImgHeight = mImgHeight;
    }

    public Captcha(int imgWidth, int imgHeight) {
        this.mImgWidth = imgWidth;
        this.mImgHeight = imgHeight;
    }

    /**
     * 获取随机字符
     *
     * @param length 长度
     * @return 随机结果
     */
    public String getRandomChars(int length) {
        if (length < 1 || length > 20) {
            length = 4;
        }
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            sb.append(mCodes.charAt(mRandom.nextInt(mCodes.length())));
        }
        return sb.toString();
    }

    /**
     * 绘制干扰线
     *
     * @param g     图形
     * @param level 干扰线等级
     */
    public void drawInterferingLind(Graphics g, int level) {
        for (int i = 0; i < level; i++) {
            int x = mRandom.nextInt(mImgWidth);
            int y = mRandom.nextInt(mImgHeight);
            int x1 = mRandom.nextInt(mImgWidth);
            int y1 = mRandom.nextInt(mImgHeight);
            g.setColor(getRandomColor());
            g.drawLine(x, y, x1, y1);
        }
    }

    /**
     * 获取随机颜色
     *
     * @return
     */
    private Color getRandomColor() {
        return new Color(mRandom.nextInt(255), mRandom.nextInt(255), mRandom.nextInt(255));
    }

    /**
     * 获取随机颜色 深色系列 用于文本显示
     *
     * @return
     */
    private Color getDarkColor() {
        return new Color(mRandom.nextInt(115), mRandom.nextInt(115), mRandom.nextInt(115));
    }

    /**
     * 获取 BufferedImage
     *
     * @return BufferedImage
     */
    private BufferedImage getImg() {
        //创建一个BufferedImage对象
        BufferedImage img = new BufferedImage(mImgWidth, mImgHeight, BufferedImage.TYPE_INT_BGR);
        Graphics g = img.getGraphics();
        //绘制背景
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, img.getWidth(), img.getHeight());
        return img;
    }

    /**
     * 绘制验证码文本
     *
     * @param g
     */
    private void drawCaptchaText(Graphics g, int textLength) {
        //获取文本
        String randomChars = getRandomChars(textLength);
        mCaptchaText = randomChars;
        //设置文本颜色
        g.setColor(getDarkColor());
        for (int i = 0; i < textLength; i++) {
            //设置随机字体风格
            Font font = getRandomFontStyle();
            /*
            首尾2个字符不进行旋转 如果旋转 可能会只出现半个字符 另一半旋转到边缘去了
             */
            if (!(i == 0 || i == textLength - 1)) {
                //设置随机旋转角度  在 -45~45°区间
                Double angle = Math.toRadians(mRandom.nextInt(90) - 45);
                //创建一个2D可旋转字体对象
                AffineTransform affineTransform = new AffineTransform();
                //设置旋转角度
                affineTransform.rotate(angle);
                //替换Font
                font = font.deriveFont(affineTransform);
            }
            g.setFont(font);
            //设置x的坐标 将布局空间分配个字符串加上一点随机偏移量
            int x = mImgWidth / textLength * i + mRandom.nextInt(mImgWidth / 4 / 2);
            //设置y的坐标 在绘制文本以文本底变为y的坐标 设置一个最低高度 以免文本太靠上消失
            int y = mRandom.nextInt(mImgHeight - font.getSize()) + font.getSize();
            g.drawString(String.valueOf(randomChars.charAt(i)), x, y);
        }
    }

    /**
     * 随机字体风格
     *
     * @return
     */
    private Font getRandomFontStyle() {
        //随机字体名称
        String fontName = mFont[mRandom.nextInt(mFont.length)];
        //随机字体大小 初始值为最小26 最大36
        int fontSize = mRandom.nextInt(10) + 26;
        //给出2/3的概率设为字体加粗
        int fontStyle = mRandom.nextInt(3) == 1 ? Font.PLAIN : Font.BOLD;
        Font font = new Font(fontName, fontStyle, fontSize);
        return font;
    }

    /**
     * 获取验证码图片
     *
     * @return
     */
    public BufferedImage getCaptchaImage(int length) {
        BufferedImage img = getImg();
        //获取图形
        Graphics g = img.getGraphics();
        //绘制干扰线 及干扰线等级
        drawInterferingLind(g, 30);
        //绘制文本 及文本个数
        drawCaptchaText(g, length);
        return img;
    }


}