10.1.2 account-captcha的主代码
account-captcha需要提供的服务是生成随机的验证码主键,然后用户可以使用这个主键要求服务生成一个验证码图片,这个图片对应的值应该是随机的,最后用户用肉眼读取图片的值,并将验证码的主键与这个值交给服务进行验证。这一服务对应的接口可以定义,如代码清单10-3所示。
代码清单10-3 AccountCaptchaService.java
package com.juvenxu.mvnbook.account.captcha;
import java.util.List;
public interface AccountCaptchaService
{
String generateCaptchaKey()
throws AccountCaptchaException;
byte[]generateCaptchaImage(String captchaKey)
throws AccountCaptchaException;
boolean validateCaptcha(String captchaKey,String captchaValue)
throws AccountCaptchaException;
List<String>getPreDefinedTexts();
void setPreDefinedTexts(List<String>preDefinedTexts);
}
很显然,generateCaptchaKey()用来生成随机的验证码主键,generateCaptchaImage()用来生成验证码图片,而validateCaptcha()用来验证用户反馈的主键和值。
该接口定义了额外的getPreDefinedTexts()和setPreDefinedTexts()方法,通过这一组方法,用户可以预定义验证码图片的内容,同时也提高了可测试性。如果AccountCaptchaService永远生成随机的验证码图片,那么没有人工的参与就很难测试该功能。现在,服务允许传入一个文本列表,这样就可以基于这些文本生成验证码,那么我们也就能控制验证码图片的内容了。
为了能够生成随机的验证码主键,引入一个RandomGenerator类,见代码清单10-4。
代码清单10-4 RandomGenerator.java
package com.juvenxu.mvnbook.account.captcha;
import java.util.Random;
public class RandomGenerator
{
private static String range="0123456789abcdefghijklmnopqrstuvwxyz";
public static synchronized String getRandomString()
{
Random random=new Random();
StringBuffer result=new StringBuffer();
for(int i=0;i<8;i++)
{
result.append(range.charAt(random.nextInt(range.length())));
}
return result.toString();
}
}
RandomGenerator类提供了一个静态且线程安全的getRandomString()方法。该方法生成一个长度为8的字符串,每个字符都是随机地从所有数字和字母中挑选,这里主要是使用了java.util.Random类,其nextInt(int n)方法会返回一个大于等于0且小于n的整数。代码中的字段range包含了所有的数字与字母,将其长度传给nextInt()方法后就能获得一个随机的下标,再调用range.charAt()就可以随机取得一个其包含的字符了。
现在看AccountCaptchaService的实现类AccountCaptchaServiceImpl。首先需要初始化验证码图片生成器,见代码清单10-5。
代码清单10-5 AccountCaptchaServiceImpl.java的afterPropertySet()方法
package com.juvenxu.mvnbook.account.captcha;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.imageio.ImageIO;
import org.springframework.beans.factory.InitializingBean;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
public class AccountCaptchaServiceImpl
implements AccountCaptchaService,InitializingBean
{
private DefaultKaptcha producer;
public void afterPropertiesSet()
throws Exception
{
producer=new DefaultKaptcha();
producer.setConfig(new Config(new Properties()));
}
……
AccountCaptchaServiceImpl实现了SpringFramework的InitializingBean接口,该接口定义了一个方法afterPropertiesSet(),该方法会被SpringFramework初始化对象的时候调用。该代码清单中使用该方法初始化验证码生成器producer,并且为producer提供了默认的配置。
接着AccountCaptchaServiceImpl需要实现generateCaptchaKey()方法,见代码清单10-6。
代码清单10-6 AccountCaptchaServiceImpl.java的generateCaptchaKey()方法
private Map<String,String>captchaMap=new HashMap<String,String>();
private List<String>preDefinedTexts;
private int textCount=0;
public String generateCaptchaKey()
{
String key=RandomGenerator.getRandomString();
String value=getCaptchaText();
captchaMap.put(key,value);
return key;
}
public List<String>getPreDefinedTexts()
{
return preDefinedTexts;
}
public void setPreDefinedTexts(List<String>preDefinedTexts)
{
this.preDefinedTexts=preDefinedTexts;
}
private String getCaptchaText()
{
if(preDefinedTexts!=null&&!preDefinedTexts.isEmpty())
{
String text=preDefinedTexts.get(textCount);
textCount=(textCount+1)%preDefinedTexts.size();
return text;
}
else
{
return producer.createText();
}
}
上述代码清单中的generateCaptchaKey()首先生成一个随机的验证码主键,每个主键将和一个验证码字符串相关联,然后这组关联会被存储到captchaMap中以备将来验证。主键的目的仅仅是标识验证码图片,其本身没有实际的意义。代码清单中的getCaptchaText()用来生成验证码字符串,当preDefinedTexts不存在或者为空的时候,就是用验证码图片生成器producer创建一个随机的字符串,当preDefinedTexts不为空的时候,就顺序地循环该字符串列表读取值。preDefinedTexts有其对应的一组get和set方法,这样就能让用户预定义验证码字符串的值。
有了验证码图片的主键,AccountCaptchaServiceImpl就需要实现generateCaptchaImage()方法来生成验证码图片,见代码清单10-7。
代码清单10-7 AccountCaptchaServiceImpl.java的generateCaptchaImage()方法
public byte[]generateCaptchaImage(String captchaKey)
throws AccountCaptchaException
{
String text=captchaMap.get(captchaKey);
if(text==null)
{
throw new AccountCaptchaException("Captch key'"+captchaKey+"'not
found!");
}
BufferedImage image=producer.createImage(text);
ByteArrayOutputStream out=new ByteArrayOutputStream();
try
{
ImageIO.write(image,"jpg",out);
}
catch(IOException e)
{
throw new AccountCaptchaException("Failed to write captcha stream!",e);
}
return out.toByteArray();
}
为了生成验证码图片,就必须先得到验证码字符串的值,代码清单中通过使用主键来查询captchaMap获得该值,如果值不存在,就抛出异常。有了验证码字符串的值之后,generateCaptchaImage()方法就能通过producer来生成一个BufferedImage,随后的代码将这个图片对象转换成jpg格式的字节数组并返回。有了该字节数组,用户就能随意地将其保存成文件,或者在网页上显示。
最后是简单的验证过程,见代码清单10-8。
代码清单10-8 AccountCaptchaServiceImpl.java的validateCaptcha()方法
public boolean validateCaptcha(String captchaKey,String captchaValue)
throws AccountCaptchaException
{
String text=captchaMap.get(captchaKey);
if(text==null)
{
throw new AccountCaptchaException("Captch key'"+captchaKey+"'not
found!");
}
if(text.equals(captchaValue))
{
captchaMap.remove(captchaKey);
return true;
}
else
{
return false;
}
}
用户得到了验证码图片以及主键后,就会识别图片中所包含的字符串信息,然后将此验证码的值与主键一起反馈给validateCaptcha()方法以进行验证。validateCaptcha()通过主键找到正确的验证码值,然后与用户提供的值进行比对,如果成功,则返回true。
当然,还需要一个SpringFramework的配置文件,它在资源目录src/main/resources/下,名为account-captcha.xml,见代码清单10-9。
代码清单10-9 account-captcha.xml
<?xml version="1.0"encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="accountCaptchaService"
class="com.juvenxu.mvnbook.account.captcha.AccountCaptchaServiceImpl">
</bean>
</beans>
这是一个最简单的SpringFramework配置,它定义了一个id为accountCaptchaService的bean,其实现为刚才讨论的AccountCaptchaServiceImpl。