
Kaptcha验证码半透明背景的实现
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代码,找到解决方法。要实现半透明背景,其实有多种方法实现:
- 方案0: 重新修改源代码,将背景部分代码直接修改后,重新打包,成为分离包。这种简单粗暴的方法比较直接,不用费太多时间,但不是长久之计。因为kaptcha是一个开源的库,一旦kaptcha升级,分离包也要跟着修改,否则就可能存在隐患。
- 方案1:直接建立新的Producer,代替DefaultKaptcha,实现背景透明。
- 方案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应用中透明背景效果,比直接修改源代码更适合程序员跟踪和优雅使用开源库。实际使用中,可以选择其中一种即可实现透明背景。
由于随机数生成类引起的安全漏洞,原kaptcha 2.3.2的仓库没有更改,重新编译kaptcha比较麻烦。
在调用时,我们为了替换原有的不安全实现类,创建了新的实现类代替原有类,完成了不替换kaptcha包修补漏洞的目的。
另外:一些提问的问题,这次也试图解决。如加入package后报错。
版权声明
内容为作者倾心创作,成果虽小,希望能为你的工作和学习带来一丝方便。苔花如米,也待芳华。
欢迎阅读、转载,请保留版权声明和源文链接,以为作者继续贡献之动力。
请保留文章来源: https://blog.csdn.net/weixin_45335489/article/details/94206751
代码之旅,一生何求!
更多推荐








所有评论(0)