java进阶验证码
1.1 验证码的作用 通常情况下,浏览器都是应用HTML标准与网站服务器动态联接的,而在HTML的表单中,基本上都是使用指定Action的POST方法提交数据。这就很容易被一些别有用心的人利用,他们可以利用机器人程序或是盗用Action的恶意程序,实现批量注册或是登录尝试,从而攻击网站或是盗取他人账户密码。为了有效防止黑客对某一特定网站进行暴力破解或攻击,可以加入验证码技术。应用验证码技术后,表单上将多出一个验证码输入框,及随机输出的验证码图片,这时再采用注册机类的暴力程序时,将因为无法读取验证码而无能为力了,从而有效保障网站安全。目前很多网站都使用了验证码。 1.2 图文验证码的原理 图文验证码是现在比较常用的的一种验证码,因为这类验证码相对比较安全。下面将介绍JSP中实现图文验证码的基本原理。 在Servlet中随机生成一个指定位置的验证码,通常情况为4位,然后把该验证码保存到Session中,再通过Java的绘图类以图片的形式输出该验证码,为了增加验证码安全级别,还可以同时输出相应的干扰线,最后在用户提交数据时,在服务器端将用户输入的验证码和Session中保存的验证码进行比较,并根据比较结果判断用户是否为合法使用网站功能。 1.3 比较常见的几种验证码 虽然说验证码可以保障网站安全,但具体的安全程度还是要根据验证码的安全级别而定。下面将对几种常见验证码进行介绍,并比较其安全级别。 文本验证码 由随机的一组数字组成,以文本形式返回,如图1所示,这也是最原始的验证码,但验证作用几乎为零,攻击者只需要简单提取文本验证码自动填上就可以了。
图1 文本验证码
简易的图片验证码 由一组随机的数字或是字母组成,以图片形式返回,如图2所示,这才是真正有效的验证码,虽然这种验证码的安全级别要高于文本验证码,但世上的事物总是矛与盾的较量。攻击者还可以用图片识别技术识别图片上的字符,把图片字符还原为文本字符。
图2 简易的图片验证码 加入干扰线的图片验证码 加入干扰线的验证码也是由一组随机的数字或字母组成,以图片的形式返回,所不同的是加入了干扰线,如图3所示,这就给攻击者进行图片识别增加了难度。这才是真正实用的验证码。
图3 加入干扰线的图片验证码 单击验证码输入框才生成并显示的验证码 单击验证码输入框才生成并显示的验证码,也是由一组随机的数字或字母组成,以图片的形式返回,所不同的是,在页面显示时,该验证码并没有生成并显示,而是当用户单击验证码输入框时,才生成并显示的,这就有效的阻止了利用机器人程序攻击网站的行为。这种类型的验证码虽然给用户操作带来了麻烦,但是对于保障网站的安全,还是比较有效的。
图4 单击验证码输入框时才生成并显示的验证码
关键技术 1.1 生成随机数技术 在Java中,提供了一种可以生成随机数据的类,那就是java.util.Random类。在使用该类时,需要通过实例化一个Random对象创建一个随机数据生成器,其语法格式如下: Random r=new Rnadom(); 其中,r是指定Random对象。 以这种方式实例化对象时,Java编译器以系统当前时间作为随机数生成器的种子,因为每时每刻的时间不可能相同,所以产生的随机数也将不同,但是如果运行速度太快,也会产生两次运行结果相同的随机数。这时,可以在实例化Random对象时,设置随机数生成器的种子,其语法格式如下: Rnadom r=new Random(seedValue); 其中,r是指定Random对象,seedValue是指随机数生成器的种子。 例如,生成以当前时间加1作为随机数生成器的种子实例化Random对象的代码如下: Random r =new Random(newjava.util.Date().getTime()+1); 在Random类中提供了获取各种数据类型随机数的方法,下面列举几个常用的方法。 public intnextInt()方法:返回一个随机整数。 public intnextInt(int n)方法:返回一个大于等于0小于n的随机整数。 public longnextLong()方法:返回一个随机长整型值。 public booleannextBoolean()方法:返回一个随机布尔型值。 public floatnextFloat()方法:返回一个随机浮点型值。 public doublenextDouble()方法:返回一个随机双精度型值。 public doublenextGaussian()方法:返回一个概率密度为高斯分布的双精度型值。 例如,生成以当前时间加1作为随机数生成器的种子,生成随机整数的代码如下: Random r =new Random(newjava.util.Date().getTime()+1); System.out.println(r.nextInt()); 运行上面的代码将生成如图1所示的随机数。
图1 生成的随机整数 1.2 随机生成汉字 例001 英文、数字和中文混合的彩色验证码 要实现随机生成汉字有两种方法,一种是将所需要的汉字保存到数组或是数据库,然后随机取出几个汉字组合就可以了,另一种是不需要借助数组或是数据库,而直接根据汉字的编码原理,随机生成汉字的区位码再转化为汉字就可以了。对于这两种方法,虽然第一种方法实现起来比较简单,但实际应用时,还是第二种方法比较实用。下面将介绍如何应用第二种方法随机生成汉字,应用这种方法随机生成汉字,还需要了解汉字的编码原理。下面将对汉字的编码原理进行简要介绍。 1980年,为了使每一个汉字有一个全国统一的代码,我国制定了“中华人民共和国国家标准信息交换汉字编码”,标准代号为GB2312—80,这种编码又称为国标码。在国标码的字符集中共收录了一级汉字3755个,二级汉字3008个,图形符号682个,三项字符总计7445个。 在国标GB2312—80中规定,所有的国标汉字及符号分配在一个94行、94列的方阵中,方阵的每一行称为一个“区”,编号为01区到94区,每一列称为一个“位”,编号为01位到94位,方阵中的每一个汉字和符号所在的区号和位号组合在一起形成的四个阿拉伯数字就是它们的“区位码”。区位码的前两位是它的区号,后两位是它的位号。用区位码就可以唯一地确定一个汉字或符号,反过来说,任何一个汉字或符号也都对应着一个唯一的区位码。例如,汉字“辉”字的区位码是2752,表明它在方阵的27区52位。 所有的汉字和符号所在的区分为以下四个组: 01区到15区 图形符号区,其中01区到09区为标准符号区,10区到15区为自定义符号区。 16区到55区 一级常用汉字区,包括了3755个一统汉字。这40个区中的汉字是按汉语拼音排序的,同音字按笔划顺序排序。其中55区的90一94位未定义汉字。 56区到87区 二级汉字区,包括了3008个二级汉字,按部首排序。 88区到94区 自定义汉字区。 其中,第10区到第15区的自定义符号区和第88区到第94区的自定义汉字区可由用户自行定义国标码中未定义的符号和汉字。 与汉字的区位码类似的还有汉字机内码,汉字的机内码是在汉字的区位码的区码和位码上分别加上A0H(这里的H表示前两位数字为十六进制数)而得到的。使用机内码表示的一个汉字占用两个字节,分别称为高位字节和低位字节,这两位字节的机内码按以下规则表示。 高位字节=区码+20H+80H(或区码+A0H) 低位字节=位码+20H+80H(或位码+A0H) 例如,汉字“啊”的区位码为1601,区码和位码分别用十六进制表示即为1001H,它的机内码的高位字节为B0H,低位字节为A1H,机内码就是B0A1H。 注意:汉字的机内码都从第16区B0开始,并且从区位D7开始以后的汉字都是和很难见到的繁杂汉字,可以将这些汉字排除掉。所以随机生成的汉字机内码的第1位范围在B、C、D之间,如果第1位是D,则第2位区位码就不能是7以后的十六进制数。由于每个区的第一个位置和最后一个位置是空的,没有汉字,因此随机生成的区位码的第3位如果是A,第4位就不能是0;第3位如果是F,第4位就不能是F。 在了解了汉字的编码原理后,就可以随机生成汉字了。在Servlet中随机生成汉字的具体步骤如下。 (1)分别随机生成汉字的区位码,并注意控制其有效性,关键代码如下: 例程01 例001\PictureCheckCode.java
String ctmp = "";
String[] rBase = { "0", "1", "2", "3", "4", "5", "6", "7", "8","9", "a", "b", "c", "d", "e", "f" };
// 生成第1位的区码
int r1 = random.nextInt(3) + 11; //生成11到14之间的随机数
String str_r1 = rBase[r1];
// 生成第2位的区码
int r2;
if (r1 == 13) {
r2 = random.nextInt(7); //生成0到7之间的随机数
} else {
r2 = random.nextInt(16); //生成0到16之间的随机数
}
String str_r2 = rBase[r2];
// 生成第1位的位码
int r3 = random.nextInt(6) + 10; //生成10到16之间的随机数
String str_r3 = rBase[r3];
// 生成第2位的位码
int r4;
if (r3 == 10) {
r4 = random.nextInt(15) + 1; //生成1到16之间的随机数
} else if (r3 == 15) {
r4 = random.nextInt(15); //生成0到15之间的随机数
} else {
r4 = random.nextInt(16); //生成0到16之间的随机数
}
String str_r4 = rBase[r4];
(2)将生成的机内码转换为汉字,具体代码如下: 例程02 例001\PictureCheckCode.java
byte[] bytes = new byte[2];
//将生成的区码保存到字节数组的第1个元素中
String str_r12 = str_r1 + str_r2;
int tempLow = Integer.parseInt(str_r12, 16);
bytes[0] = (byte) tempLow;
//将生成的位码保存到字节数组的第2个元素中
String str_r34 = str_r3 + str_r4;
int tempHigh = Integer.parseInt(str_r34, 16);
bytes[1] = (byte) tempHigh;
ctmp = new String(bytes);//根据字节数组生成汉字
System.out.println("生成汉字:" + ctmp);
运行结果如图2和图3所示。
图2 随机生成的汉字
图3 应用到验证码中显示的效果
1.3 Ajax重构 例001 英文、数字和中文混合的彩色验证码 随着Ajax应用程序的不断扩展,将会有越来越多的JavaScript代码应用到Ajax中,这可能导致许多意想不到的问题。因此有必要对Ajax代码进行重构。下面将介绍实现Ajax重构的基本步骤。 (1)创建一个单独的JS文件,名称为AjaxRequest.js,并且在该文件中编写重构Ajax所需的代码,具体代码如下: 例程03 例001\AjaxRequest.js
//编写构造函数
net.AjaxRequest=function(url,onload,onerror,method,params){
this.req=null;
this.οnlοad=onload;
this.οnerrοr=(onerror) ? onerror : this.defaultError;
this.loadDate(url,method,params);
}
//编写用于初始化XMLHttpRequest对象并指定处理函数,最后发送HTTP请求的方法
net.AjaxRequest.prototype.loadDate=function(url,method,params){
if (!method){
method="GET";
}
if (window.XMLHttpRequest){
this.req=new XMLHttpRequest();
} else if (window.ActiveXObject){
this.req=new ActiveXObject("Microsoft.XMLHTTP");
}
if (this.req){
try{
var loader=this;
this.req.onreadystatechange=function(){
net.AjaxRequest.onReadyState.call(loader);
}
this.req.open(method,url,true);
if(method=="POST"){
this.req.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
}
this.req.send(params);
}catch (err){
this.onerror.call(this);
}
}
}
//重构回调函数
net.AjaxRequest.onReadyState=function(){
var req=this.req;
var ready=req.readyState;
if (ready==4){
if (req.status==200 ){
this.onload.call(this);
}else{
this.onerror.call(this);
}
}
}
//重构默认的错误处理函籹
net.AjaxRequest.prototype.defaultError=function(){
alert("error fetching data!"
+"\n\nreadyState:"+this.req.readyState
+"\nstatus: "+this.req.status
+"\nheaders: "+this.req.getAllResponseHeaders());
}
(2)在需要应用Ajax的页面中应用以下的语句包括步骤(1)中创建的JS文件。 例程04 例001\index.jsp(3)在应用Ajax的页面中编写错误处理的方法、实例化Ajax对象的方法和回调函数,具体代码如下: 例程05 例001\index.jsp
说明:如果在同一个页面中需要多次调用Ajax,只需要编写多个实例化Ajax对象的方法和回调函数即可,如果对于所的错误提示都是一样的,可以不必编写多个错误处理方法。
1.4 图片缩放和旋转
在Java中提供了java.awt.geom.AffineTransform类,用来实现对图片进行缩放或旋转操作。如果要实现这些操作,首先需要使用AffineTransform类创建一个对象,具体代码如下:
AffineTransform trans = newAffineTransform();
然后再使用AffineTransform类提供的相关方法对图片进行缩放或旋转。其中,对图片进行缩放的方法为scale(double a,double b),对图片进行旋转的方法为rotate(doublenumber,double x,double y),下面将对这两个方法进行详细介绍。
scale(double a,double b)方法:用于将图片在x轴方向缩放a倍,y轴方向缩放b倍。例如,将图片在x轴缩放1.1倍,y轴方向缩放1.3倍的具体代码如下:
AffineTransform trans = newAffineTransform();
trans.scale(1.1, 1.3);
运行结果如图4所示。
图4 对图片进行缩放 rotate(double number,double x,double y)方法:用于将图片沿顺时针或逆时针以(x,y)为轴点旋转number个弧度。例如,将图片以(x,y)为轴点旋转30个弧度的具体代码如下: AffineTransform trans = newAffineTransform(); trans.rotate(30 * 3.14 / 180,20, 7); 运行结果如图5所示。
图5 对图片进行旋转 1.5 随机绘制干扰线(折线) 例002 Ajax实现的无刷新的彩色验证码 Graphics类中提供了绘制折线的方法drawPolyline(),该方法的原形如下: Public abstract voiddrawPolyline(int[] xPoints,int[] yPoints,int nPoints) 使用当前颜色绘制一系列相互连接的线段,线段端点的坐标由数组xPoints和yPoints指定,线段的端点个数由nPoints指定。 例如,随机绘制一条如图6所示的折线的关键代码如下: 例程06 例002\PictureCheckCode.java
Graphics g = image.getGraphics();
Random random = new Random();
int[] xPoints=new int[3];
int[] yPoints=new int[3];
for(int j=0;j<3;j++){
xPoints[j]=random.nextInt(width - 1);
yPoints[j]=random.nextInt(height - 1);
}
g.drawPolyline(xPoints, yPoints,3);
图6 绘制随机折线 1.6 MD5加密技术 MD5的全称是Message-Digest Algorithm 5,在90年代初由MIT的计算机科学实验室和RSA Data Security Inc发明,经MD2、MD3和MD4发展而来。 Message-Digest泛指任意长度的字节串(Message)的Hash变换,就是把一个任意长度的字节串变换成一定长度的整数。请 注意,这里是“字节串”而不是“字符串”,因为这种变换只与字节的值有关,而与字符集或编码方式无关。 MD5将任意长度的“字节串”变换成一个128bit的大整数,并且它是一个不可逆的字符串变换算法。换句话说,即使看到源程序和算法描述,也无法将一个MD5值变换回原始的字符串。从数学原理上说,是因为原始的字符串有无穷多个,这有点像不存在反函数的数学函数。 Java语言已经实现了MD5加密技术。这可以通过MessageDigest类创建一个能够进行MD5加密的对象实现,其具体实现步骤如下: (1)通过java.security.MessageDigest类的静态方法getInstance返回继承了MessageDigest类的对象,如果要返回一个使用MD5算法的对象,可以传入字符串MD5,具体代码如下: MessageDigest code =MessageDigest.getInstance("MD5"); (2)获取MessageDigest对象后,可以通过调用update方法,将要加密信息中的所有字节提供给该对象,关键代码如下: String str="M7R6"; code.update(str.getBytes()); (3)调用digest方法完成消息摘要的计算,并以字节数组的形式返回消息摘要,关键代码如下: byte[] bs = code.digest(); (4)将加密后的字节数组转换成十六进制的字符串,形成最终的密文,关键代码如下: for (int i = 0; iint v = bs[i] & 0xFF; if (v < 16) { sb.append(0); } sb.append(Integer.toHexString(v)); } 例如,对字符串M7R6进行MD5加密后,将得到如图7所示的密文。
图7 对字符串进行加密