|
验证码的背后(基于JDK 1.4.2)
现在很多站点为了防止口令的暴力破解,或者BBS上为了防止使用程序发垃圾帖子,都加上了验证码的功能,这个大家可能也都有体验,就是随机出几个数字,然后在内存中画出来,再加上一些扰乱线,有的甚至还加入了变形,然后输出到页面。这样的图片人眼基本上可以识别,但是如果使用OCR之类的程序识别率就会比较低。
如何用JSP实现这个功能?在网络上可以搜索出很多,下面有一个估计是最常见的,也是大家用得最多的:
package test;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletException;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
import java.io.IOException;
/**
* @author <a href="mailto:jade_yuan@hotmail.com">Jade Yuan</a>
* @version 2006-3-9 18:34:36
*/
public class ImageServlet extends HttpServlet {
private static final String CONTENT_TYPE = "image/jpeg";
// private static final String [] FontNames={"SansSerif","Serif","MonoSpaced","Dialog","DialogInput"};
//Initialize global variables
public void init() throws ServletException {
}
//Process the HTTP Get request
public void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
response.setContentType(CONTENT_TYPE);
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 在内存中创建图象
int width = 60, height = 20;
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.createGraphics();
//生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
//设定字体
g.setFont(new Font("DialogInput", Font.ITALIC, 18));
// g.setFont(new Font(FontNames[random.nextInt(6)%5], Font.ITALIC, 18));
//画边框
//g.setColor(new Color());
//g.drawRect(0,0,width-1,height-1);
// 随机产生255条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 255; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 取随机产生的认证码(4位数字)
//String rand = request.getParameter("rand");
//rand = rand.substring(0,rand.indexOf("."));
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
// 将认证码显示到图象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110),
20 + random.nextInt(110))); //调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
g.drawString(rand, 13 * i + 6, 16);
}
//System.out.println("validate code=" + sRand);
// 将认证码存入SESSION
HttpSession session = request.getSession();
session.setAttribute("validecode", sRand);
// 图象生效
g.dispose();
// 输出图象到页面
ServletOutputStream outputstream = response.getOutputStream();
ImageIO.write(image, "JPEG", outputstream);
}
//Clean up resources
public void destroy() {
}
Color getRandColor(int fc, int bc) { //给定范围获得随机颜色
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
public static void main(String[] args) {
}
}
一般来说,验证码通常出现在登录的首页,一旦我们顺利登录系统,就开始使用相应功能,不再出现验证码的页面。这个时候你可能感觉不出来什么。但是如果有人频繁的刷新这个出现验证码的页面呢?事实是:你的系统几乎难以再对外提供服务了。我就遇到了这样的系统,一开始非常难以定位原因,当时也不知道有人频繁刷新这个页面。直到在Thread dump中看到了System.gc()的字样!是由com.sun.imageio.plugins.jpeg.JPEGImageWriter.setOutput()方法调用的。于是,在启动脚本中加上了-verbose:gc,观察输出,结果可想而知,每次调用这个servlet,就会做一次System.gc(),就是Full GC了,如果频繁的请求这个页面,那么系统肯定陷入了无法正常响应的情况。
于是乎,打开JDK 1.4.2 的源码,com.sun.imageio.plugins.jpeg.JPEGImageWriter.setOutput()是这样写的:
public void setOutput(Object output) {
super.setOutput(output); // validates output
ios = (ImageOutputStream) output; // so this will always work
// Set the native destination
setDest(structPointer, ios);
srcRas = null;
raster = null;
currentImage = 0;
System.gc();
}
为什么这个Sun 的JDK自带的类会这么写呢?可能是编写者担心JPEG图片尺寸大的时候占用太多的内存吧,所以及时的显式调用System.gc()。
问题找到了,那么就需要找到解决方案。一开始我打算自己读出Image的byte[],再写出到输出流中,但是找了半天也没有找到好的办法。
没有办法了,转换思路吧,先研读一下PNGImageWriter的代码,发现没有System.gc()的调用,那就用PNG的格式输出一下试试。
修改了
private static final String CONTENT_TYPE = "image/jpeg";
及
ImageIO.write(image, "JPEG", outputstream);
两行代码中JPEG为PNG,测试一下:
成功!
其实还有一点,我补充一下,Sun 的 Hotspot有一个参数XX:+DisableExplicitGC可以禁用显式的GC,也就是System.gc()的调用,但是不建议使用,尤其是对于运行在中间件上的应用,如果使用了这个参数,说不定可能引发其他的问题。
另外,有热心网友的评论,我也一并整理过来了,非常感谢他们。(大家不要和我要稿费啊~~)
dkmilk:
我也补充一点:
很多系统在生成验证码后,把字符写入session,然后进行比较。
但在比较结束后一般就根本没有注意去清空session.
这样就造成一种现象:只要不去刷新生成验证码的URL,SESSION中的数字永远是一样的。这样就从根本上失去了验证码的作用。我测试过几个较大型的站点,都有这个问题。
所以说程序员也应该多多少少懂些常见的破解技术。
stevegy:
呵呵,楼主还是应该说明一下JDK的版本的,最新的1.5.0_09已经没有这样的代码了。
|