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。