Kaptcha验证码半透明背景的实现

Kaptcha库

验证码是防止机器尝试的重要手段,Kaptcha是常用的一个库,从其声明看,可能来自google,因为种种原因,目前依赖更多的是com.gitpuh.penggle.kaptcha,版本为2.3.2。POM中依赖代码

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

本次修改针对安全漏洞CVE-2018-18531,提供了无损解决方案
背景:
kaptcha 2.3.2版本中的多个文件存在安全漏洞,该漏洞源于程序使用‘Random’函数(而不是‘SecureRandom’函数)创建CAPTCHA值。远程攻击者可借助暴力破解的方法利用该漏洞绕过访问限制。(多个文件包括:text/impl/DefaultTextCreator.java、text/impl/ChineseTextProducer.java和text/impl/FiveLetterFirstNameTextCreator.java)

kaptcha选项无法设置背景透明?

Kaptcha可配置的选项有:

  • 验证码文本的字体
  • 验证码文本的大小
  • 验证码文本的颜色
  • 验证码文本的范围(数字,字母,中文汉字!)
  • 验证码图片的大小,边框,边框粗细,边框颜色
  • 验证码的干扰线
  • 验证码的样式(鱼眼样式、3D、普通模糊、…)

Kaptcha有诸多配置选项,可以简单修改配置项的值就可以达到预期的效果。
但是验证码的背景透明效果,是一个稍复杂的特性,没有直接的配置项可以设置,只要一个背景实现项kaptcha.background.impl。
尝试简单配置kaptcha.background.impl,比如复制为’', null,或者translucent,都没有得到透明效果。难道kaptcha可配置特性中没有背景透明这个选项吗?

博主也是个拿来主义重症患者,尝试到在网上查找kaptcha 背景透明效果的方法,但是查找了很多介绍Kaptcha配置选项的文章,都没有解决,甚至没有人提过这件事儿。

如果不使用半透明背景,页面背景中间的校验码部分不能融合在页面中,像一块创可贴粘在屏幕上,非常难看。聪明的设计师可以把背景颜色设为暗色,匹配页面的颜色。但是当下一个项目页面改为浅色背景时,重新修改配置或代码,还需要修改代码或配置文件。
在这里插入图片描述
而使用了半透明效果后,就显得舒服多了:
在这里插入图片描述

如何实现半透明背景呢?

我们可以查看kaptcha代码,找到解决方法。要实现半透明背景,其实有多种方法实现:

  1. 方案0: 重新修改源代码,将背景部分代码直接修改后,重新打包,成为分离包。这种简单粗暴的方法比较直接,不用费太多时间,但不是长久之计。因为kaptcha是一个开源的库,一旦kaptcha升级,分离包也要跟着修改,否则就可能存在隐患。
  2. 方案1:直接建立新的Producer,代替DefaultKaptcha,实现背景透明。
  3. 方案2:使用定制的BackgroundProducer的实现类,配合kaptcha.background.impl配置选项,实现背景透明。

kaptcha的配置选项比较丰富,简单的配置项通过赋值就可以实现效果,比较复杂的配置项需要加入新建的实现累。如:背景实现类 kaptcha.background.impl,文字渲染实现类 kaptcha.word.impl,干扰实现类 kaptcha.noise.impl,文本生成实现类 kaptcha.textproducer.impl,图片处理实现类 kaptcha.producer.impl等,这些复杂的实现类配置选项,需要定制单独的实现类实现个性化定制。

方案1和方案2就是利用了kaptcha构架中的灵活配置特性,实现半透明背景方案。

方案1 直接创建新的KaptchaProducer

Producer是输出验证码的主类,原有的DefaultProducer缺省实现类为DefaultKaptcha,在其createImage()方法中调用背景处理类BackgroundProducer进行背景渲染。背景处理类 实例创建策略比较简单,是根据配置项kaptcha.background.impl的值进行创建,如果出现非预期的值,就返回缺省背景处理类DefaultKaptcha,DefaultKaptcha就是不透明的背景的主要来源。对,就是这句:

BackgroundProducer backgroundProducer = getConfig().getBackgroundImpl();
//然后渲染背景 
bi = backgroundProducer.addBackground(bi);

下面这段代码可以看出,getBackgroundImpl()简单取得配置项kaptcha.background.impl的值,判断值为空,就加载建立缺省背景处理类实例;如果值不为空,就认为它是一个实现类去建立实例。

	/** */
	public BackgroundProducer getBackgroundImpl()
	{
		String paramName = Constants.KAPTCHA_BACKGROUND_IMPL;
		String paramValue = this.properties.getProperty(paramName);
		BackgroundProducer backgroundProducer = (BackgroundProducer) this.helper.getClassInstance(paramName, paramValue,
				new DefaultBackground(), this);
		return backgroundProducer;
	}

方案1的关键就是判断配置项kaptcha.background.impl的值为空时,不加载任何背景处理类实例,所以也就不必进行渲染背景操作,所以也就没有背景,背景就变得透明了。即,不加载,不渲染,变透明。

方案1修改了kaptcha.background.impl的值为空的行为,同时,也做了一些兼容处理,如果kaptcha.background.impl有正确的值,如后续建立的TranslucentBackground或者内置的DefaultBackground,则 this.getConfig().getBackgroundImpl()会加载对应类实例,渲染出透明背景(TranslucentBackground)或渐变背景(DefaultBackground)。

        if ( paramValue != null && !paramValue.isEmpty()) {
            BackgroundProducer backgroundProducer = this.getConfig().getBackgroundImpl();
                bi = backgroundProducer.addBackground(bi);
        }
        //如果paramValue为空,则不加载,不渲染

原有DefaultKaptcha类中createImage()方法代码:

	/**
	 * Create an image which will have written a distorted text.
	 * 
	 * @param text
	 *            the distorted characters
	 * @return image with the text
	 */
	public BufferedImage createImage(String text)
	{
		WordRenderer wordRenderer = getConfig().getWordRendererImpl();
		GimpyEngine gimpyEngine = getConfig().getObscurificatorImpl();
		BackgroundProducer backgroundProducer = getConfig().getBackgroundImpl();
		boolean isBorderDrawn = getConfig().isBorderDrawn();
		this.width = getConfig().getWidth();
		this.height = getConfig().getHeight();

		BufferedImage bi = wordRenderer.renderWord(text, width, height);
		bi = gimpyEngine.getDistortedImage(bi);
		bi = backgroundProducer.addBackground(bi);
		Graphics2D graphics = bi.createGraphics();
		if (isBorderDrawn)
		{
			drawBox(graphics);
		}
		return bi;
	}

新实现类 TranslucentKaptcha中createImage(String text) 的代码:

    public BufferedImage createImage(String text) {
        WordRenderer wordRenderer = this.getConfig().getWordRendererImpl();
        GimpyEngine gimpyEngine = this.getConfig().getObscurificatorImpl();

        boolean isBorderDrawn = this.getConfig().isBorderDrawn();
        this.width = this.getConfig().getWidth();
        this.height = this.getConfig().getHeight();
        BufferedImage bi = wordRenderer.renderWord(text, this.width, this.height);
        bi = gimpyEngine.getDistortedImage(bi);

        String paramName = Constants.KAPTCHA_BACKGROUND_IMPL;
        String paramValue = this.getConfig().getProperties().getProperty(paramName);

        if ( paramValue != null && !paramValue.isEmpty()) {
            BackgroundProducer backgroundProducer = this.getConfig().getBackgroundImpl();
                bi = backgroundProducer.addBackground(bi);
        }

        Graphics2D graphics = bi.createGraphics();
        if (isBorderDrawn) {
            this.drawBox(graphics);
        }

        return bi;
    }

方案1的Producer实例类TranslucentKaptcha的完整代码

package KaptchaBg;

import com.google.code.kaptcha.BackgroundProducer;
import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.GimpyEngine;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.text.WordRenderer;
import com.google.code.kaptcha.util.Configurable;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.awt.geom.Line2D.Double;
import java.awt.image.BufferedImage;

public class TranslucentKaptcha extends Configurable implements Producer {
    private int width = 200;
    private int height = 50;

    public TranslucentKaptcha() {
    }

    public BufferedImage createImage(String text) {
        WordRenderer wordRenderer = this.getConfig().getWordRendererImpl();
        GimpyEngine gimpyEngine = this.getConfig().getObscurificatorImpl();

        boolean isBorderDrawn = this.getConfig().isBorderDrawn();
        this.width = this.getConfig().getWidth();
        this.height = this.getConfig().getHeight();
        BufferedImage bi = wordRenderer.renderWord(text, this.width, this.height);
        bi = gimpyEngine.getDistortedImage(bi);

        String paramName = Constants.KAPTCHA_BACKGROUND_IMPL;
        String paramValue = this.getConfig().getProperties().getProperty(paramName);

        if ( paramValue != null && !paramValue.isEmpty()) {
            BackgroundProducer backgroundProducer = this.getConfig().getBackgroundImpl();
                bi = backgroundProducer.addBackground(bi);
        }

        Graphics2D graphics = bi.createGraphics();
        if (isBorderDrawn) {
            this.drawBox(graphics);
        }
        return bi;
    }

    private void drawBox(Graphics2D graphics) {
        Color borderColor = this.getConfig().getBorderColor();
        int borderThickness = this.getConfig().getBorderThickness();
        graphics.setColor(borderColor);
        if (borderThickness != 1) {
            BasicStroke stroke = new BasicStroke((float) borderThickness);
            graphics.setStroke(stroke);
        }

        Line2D line1 = new Line2D.Double(0.0D, 0.0D, 0.0D, (double) this.width);
        graphics.draw(line1);
        Line2D line2 = new Line2D.Double(0.0D, 0.0D, (double) this.width, 0.0D);
        graphics.draw(line2);
        line2 = new Line2D.Double(0.0D, (double) (this.height - 1), (double) this.width, (double) (this.height - 1));
        graphics.draw(line2);
        line2 = new Line2D.Double((double) (this.width - 1), (double) (this.height - 1), (double) (this.width - 1), 0.0D);
        graphics.draw(line2);
    }

    public String createText() {
        return this.getConfig().getTextProducerImpl().getText();
    }
}

方案2 使用定制的BackgroundProducer的实现类

原有的BackgroundProducer缺省实现类中,使用addBackground()方法进行背景渲染,addBackground()方法的第三个参数imageType为TYPE_INT_RGB,后续又加入渐变混合,所以无法透明。

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

方案2,新建新的背景实现类TranslucentBackground,修改addBackground()方法的背景渲染部分,使用BufferedImage.TRANSLUCENT类型,并且移除了后续的渐变色,所以变成了单纯的透明色。
修改后的新代码看起来是这个样子:

            BufferedImage imageWithBackground = new BufferedImage(width, height,
                    BufferedImage.TRANSLUCENT);

新的背景实现类完成后,将配置项kaptcha.background.impl的值赋为TranslucentBackground,即可实现背景透明。
kaptcha.background.impl配置选项,下面主程序使用代码方式进行config配置,当然配置文件方式也同样处理。

properties.put("kaptcha.background.impl", "TranslucentBackground"); //参见后续KaptchaDemo中的main函数

方案2的TranslucentBackground实现类完整代码:

package KaptchaBg;

import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import com.google.code.kaptcha.BackgroundProducer;
import com.google.code.kaptcha.util.Configurable;

    /**
     * Translucent implementation of {@link BackgroundProducer}, adds a translucent
     * background to an image.
     */
    public class TranslucentBackground extends Configurable implements BackgroundProducer
    {
        /**
         * @param baseImage the base image
         * @return an image with a translucent background added to the base image.
         */
        public BufferedImage addBackground(BufferedImage baseImage)
        {
            Color colorFrom = getConfig().getBackgroundColorFrom();
            Color colorTo = getConfig().getBackgroundColorTo();

            int width = baseImage.getWidth();
            int height = baseImage.getHeight();

            // create an opaque image
            BufferedImage imageWithBackground = new BufferedImage(width, height,
                    BufferedImage.TRANSLUCENT);

            Graphics2D graph = (Graphics2D) imageWithBackground.getGraphics();
            graph.drawImage(baseImage, 0, 0, null);

            return imageWithBackground;
        }
	}

两种背景透明方案的主程序KaptchaDemo

(包括Kaptcha 2.3.2中的安全漏洞无替换原包解决方案)
KaptchaDemo 将以上两种方案,以及原始方法在一个main函数中运行,输出3个图片文件,可以比较不同的实现效果。

package KaptchaBg;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import com.google.code.kaptcha.util.Configurable;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Properties;

public class KaptchaDemo {

    public static void main(String[] args) {

        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.border.color", "105,179,90");
        properties.put("kaptcha.textproducer.font.color", "blue");
        properties.put("kaptcha.image.width", "210");
        properties.put("kaptcha.image.height", "45");
        properties.put("kaptcha.textproducer.font.size", "35");
        properties.put("kaptcha.session.key", "code");
        properties.put("kaptcha.textproducer.char.length", "15");
        properties.put("kaptcha.textproducer.font.names", "simsun, Arial, Courier");

        //Kaptcha安全漏洞CVE-2018-18531解决方案,无需重新打包,在调用时将不安全的实现类替换掉
        properties.put("kaptcha.textproducer.impl", "KaptchaBg.SecureTextCreator");
        properties.put("kaptcha.word.impl", "KaptchaBg.SecureWordRenderer");

        Config config = new Config(properties);

        KaptchaDemo demo = new KaptchaDemo();
        String text = "你好kaptcha";

        //使用原始不透明kaptcha
        Producer kap = demo.originKaptcha(config);
        BufferedImage bi = kap.createImage(text);
        demo.writeImageFile(bi, "imgs/kaptcha0.png");

        //方案1. 使用透明kaptcha,直接修改背景,不使用kaptcha.background.impl
//        properties.put("kaptcha.background.impl", "");
        Config config1 = new Config(properties);

        Producer kap1 = demo.translucentKaptchaByModified(config1);
        BufferedImage bi1 = kap1.createImage(text);
        demo.writeImageFile(bi1, "imgs/kaptcha1.png");

        //方案2. 普通的kaptcha,使用定制kaptcha.background.impl
        properties.put("kaptcha.background.impl", "KaptchaBg.TranslucentBackground");
        Config config2 = new Config(properties);

        Producer kap2 = demo.translucentKaptchaWithBkImpl(config2);
        BufferedImage bi2 = kap2.createImage(text);
        demo.writeImageFile(bi2, "imgs/kaptcha2.png");

    }

    public Producer originKaptcha(Config config ) {

        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return (Producer)defaultKaptcha;
    }

    //方案1
    public Producer translucentKaptchaByModified(Config config ) {

        Configurable translucentKaptcha = new TranslucentKaptcha();
        translucentKaptcha.setConfig(config);
        return (Producer)translucentKaptcha;
    }


    //方案2
    public Producer translucentKaptchaWithBkImpl(Config config ) {

        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return (Producer)defaultKaptcha;
    }

    public void writeImageFile(BufferedImage bi, String path) {
        File outputfile = new File(path);
        try {
            ImageIO.write(bi, "png", outputfile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

为了解决安全漏洞CVE-2018-18531的不安全的随机数,需要重新实现2个类SecureTextCreator.java和SecureWordRenderer.java,主要改进就是将
// Random rand = new Random(); 换成SecureRandom
Random rand = new SecureRandom();

SecureTextCreator.java

package KaptchaBg;


import com.google.code.kaptcha.text.TextProducer;
import com.google.code.kaptcha.util.Configurable;

import java.security.SecureRandom;
import java.util.Random;

public class SecureTextCreator extends Configurable implements TextProducer {
    public SecureTextCreator() {
    }

    public String getText() {
        int length = this.getConfig().getTextProducerCharLength();
        char[] chars = this.getConfig().getTextProducerCharString();

//        Random rand = new Random();
        Random rand = new SecureRandom();

        StringBuffer text = new StringBuffer();

        for(int i = 0; i < length; ++i) {
            text.append(chars[rand.nextInt(chars.length)]);
        }

        return text.toString();
    }
}

SecureWordRenderer.java

package KaptchaBg;

import com.google.code.kaptcha.text.WordRenderer;
import com.google.code.kaptcha.util.Configurable;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.image.BufferedImage;
import java.security.SecureRandom;
import java.util.Random;

public class SecureWordRenderer extends Configurable implements WordRenderer {
    public SecureWordRenderer() {
    }

    public BufferedImage renderWord(String word, int width, int height) {
        int fontSize = this.getConfig().getTextProducerFontSize();
        Font[] fonts = this.getConfig().getTextProducerFonts(fontSize);
        Color color = this.getConfig().getTextProducerFontColor();
        int charSpace = this.getConfig().getTextProducerCharSpace();
        BufferedImage image = new BufferedImage(width, height, 2);
        Graphics2D g2D = image.createGraphics();
        g2D.setColor(color);
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
        g2D.setRenderingHints(hints);
        FontRenderContext frc = g2D.getFontRenderContext();

//        Random random = new Random();
        Random random = new SecureRandom();

        int startPosY = (height - fontSize) / 5 + fontSize;
        char[] wordChars = word.toCharArray();
        Font[] chosenFonts = new Font[wordChars.length];
        int[] charWidths = new int[wordChars.length];
        int widthNeeded = 0;

        int startPosX;
        for(startPosX = 0; startPosX < wordChars.length; ++startPosX) {
            chosenFonts[startPosX] = fonts[random.nextInt(fonts.length)];
            char[] charToDraw = new char[]{wordChars[startPosX]};
            GlyphVector gv = chosenFonts[startPosX].createGlyphVector(frc, charToDraw);
            charWidths[startPosX] = (int)gv.getVisualBounds().getWidth();
            if (startPosX > 0) {
                widthNeeded += 2;
            }

            widthNeeded += charWidths[startPosX];
        }

        startPosX = (width - widthNeeded) / 2;

        for(int i = 0; i < wordChars.length; ++i) {
            g2D.setFont(chosenFonts[i]);
            char[] charToDraw = new char[]{wordChars[i]};
            g2D.drawChars(charToDraw, 0, charToDraw.length, startPosX, startPosY);
            startPosX = startPosX + charWidths[i] + charSpace;
        }

        return image;
    }
}

附录

kaptcha一些安全问题
kaptcha是一款基于SimpleCaptcha的验证码生成工具。

kaptcha 2.3.2版本中的多个文件存在安全漏洞,该漏洞源于程序使用‘Random’函数(而不是‘SecureRandom’函数)创建CAPTCHA值。远程攻击者可借助暴力破解的方法利用该漏洞绕过访问限制。(多个文件包括:text/impl/DefaultTextCreator.java、text/impl/ChineseTextProducer.java和text/impl/FiveLetterFirstNameTextCreator.java)

kaptcha 2.3.2版本中的多个文件存在安全漏洞

总结

本文用两种不同的方案实现了Kaptcha应用中透明背景效果,比直接修改源代码更适合程序员跟踪和优雅使用开源库。实际使用中,可以选择其中一种即可实现透明背景。
由于随机数生成类引起的安全漏洞,原kaptcha 2.3.2的仓库没有更改,重新编译kaptcha比较麻烦。
在调用时,我们为了替换原有的不安全实现类,创建了新的实现类代替原有类,完成了不替换kaptcha包修补漏洞的目的。
另外:一些提问的问题,这次也试图解决。如加入package后报错。

版权声明

内容为作者倾心创作,成果虽小,希望能为你的工作和学习带来一丝方便。苔花如米,也待芳华。
欢迎阅读、转载,请保留版权声明和源文链接,以为作者继续贡献之动力。
请保留文章来源: https://blog.csdn.net/weixin_45335489/article/details/94206751
代码之旅,一生何求!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值