跟着书里的讲解,跟着做了一遍该项目:
首先明白注册账户的需求:
账号的lD和Email地址都可以用来唯一地标识某个用户,而显示名称则用来显示在页面下,方便浏览。注册的时候用户还需要输入两次密码,以确保没有输错,系统则需要负责检查ID和email的唯一性,验证两次输入的密码是否一致,验证码是由系统随机生成的只能由肉眼识别其内容的图片,若输入正确的验证码信息,系统则会进行检查,如果验证码错误。系统会生成并返回新的验证码。所有检查
都没问题了,系统就会生成一个激活链接,并发送到用户的邮箱。单击激活链接后,账户就被激活了,这时账户注册完成,用户可以进行登录。对于一个账户注册服务,还需要考虑一些安全因素,例如,需要在服务器端密文地保存密码,检查密码的强弱程度,更进一步则需要考虑验证码的失效时间,激活链接的失效时间等等。
需求用例:
主要场景:
1、用户访问注册页面
2、系统随机生成验证码图片
3、用户输入ID、Email等注册信息
4、输入验证码
5、提交注册请求
6、系统检查验证码、检查ID的唯一性,检查邮箱是否已被注册、密码和确认密码是否一致
7、系统保存未激活的账户信息
8、系统生成激活连接,发送给用户邮箱
9、用户打开邮箱,访问激活链接
10、系统解析激活连接,激活相关用户
11、用户使用ID和密码登陆
扩展场景:
4a:用户无法看清验证码,请求重新生成 1、跳转到2
6a:系统检测到用户输入的验证码有误 1、提示验证码有错,2、跳转到2
6b:检测到 ID已被注册,邮箱,密码有误 1、提示错误信息,2、跳转到2
从上面可以看出该服务有几个接口:生成验证码图片、处理注册请求、激活账户以及处理登陆请求。
接口结构:
acountService类:
generateCaptchaKey()
generateCaptchaImage(captchakey:string)
signUp(signUpRequest:SignUpRequest):接收对象,进行验证。如果验证正确。则创建一个末被激活的账户,同时在后台也需要发送一封带有激活链接的邮件。
activate(activationNumber:string):方法接收一个激活码,查找时应的账户进行激活
login(id:string,password:string)
signUpRequest:包含用户的注册信息,表单信息:id、Email、displayName、password、comfirmpassword、captchaKey、captchaValue。
generateCaptchaKey()的简单解释就是验证码,每个captcha都需要有一个key ,根据这个key ,系统才能得到对应的验证码图片以及实际值。因此,generateCaptchaImage会生成一个captchakey使用这个key再调用generateCaptchaImage方法就能得到验证码图片。验证码的key以及验证码图片被传送到客户端,用户通过肉眼识别再输人验证码的值,伴随着key再传送到服务器端验证,服务器端就可以通过这个key查到正确的验证码值,井与客户端传过来的值进行比对验证。
模块划分:
com.hust.silence.accout.service:系统的核心,它封装了所有下层细节,对外暴露简单的接日,这实际上是一个Facade模式。
com.hust.silence.accout.web:该模块包含所有与web相关的内容,包括jsp等,直接依赖于service模块
com.hust.silence.accout.persist:处理账户信息的持久化,包括增、删、改、查等,根据实现,可以基于数据库或者文件
com.hust.silence.accout.captcha:处理验证码的key生成、图片生成以及验证等
com.hust.silence.accout.email: 处理邮件服务的配置,激活邮件的编写和发送等
配置pom.xml
加入需要的各种spring framework的模块,Greenmail是开源的邮件服务套件,Javax.mail为实现发送的一个类库。从上面的信息我们可以知道,该项目时是com.hust.silence的一个account项目,项目里有一个模块为account-email。
实现Email模块:
account-email只有一个很简单的接口。
public interface AccountEmailService { void sendMail(String to, String subject, String htmlText) throws AccountEmailException; } public class AccountEmailServiceImpl implements AccountEmailService{ private JavaMailSender javaMailSender; private String systemEmail; public void sendMail(String to, String subject, String htmlText) throws AccountEmailException { // TODO Auto-generated method stub try { MimeMessage msg = javaMailSender.createMimeMessage(); MimeMessageHelper msgHelper = new MimeMessageHelper(msg); msgHelper.setFrom(systemEmail); msgHelper.setTo(to); msgHelper.setSubject(subject); msgHelper.setText(htmlText); javaMailSender.send(msg); } catch (Exception e) { // TODO: handle exception throw new AccountEmailException("fail to send mail",e); } } //实现依赖注入 public JavaMailSender getJavaMailSender(){ return javaMailSender; } public void setJavaMailSender(JavaMailSender javaMailSender){ this.javaMailSender = javaMailSender; } public String getSystemEmail(){ return systemEmail; } public void setSystememail(String systemEmail){ this.systemEmail = systemEmail; } } public class AccountEmailException extends Exception { /** * */ private static final long serialVersionUID = 6514881539290222459L; public AccountEmailException(String message) { super(message); } public AccountEmailException(String message, Throwable throwable){ super(message, throwable); } }
配置文件:
id="propertyConfigurer":这是springframework用来帮助载入properties文件的组件,代码中表示从classpath的根目录下载入名为account-email.properties文件中的属性。
id="javaMailSender":定义邮件服务器的一些配置.包括协议、端口、主机,用户名、密码,是否需要认证等属性。这段配置还使用了propertyConfigurer的属性引用,比如host的值为$ { email.host }。之前定义的propertyConfigurer作用就在于此、可以将邮件服务器相关的配置分离到外部的properties文件中,比如可以定义这样一个properties文件。配置javaMailSender使用163:
account-email.properties(在src/test/resources文件夹里):
email.protocol=smtp
email.host=smtp.163.com
email.port=25
email.username=test@163.com
email.password=password
email.auth=true
email.systemEmail=test@163.com
测试:
只需要测试一个sendMail()接口,这个就需要准备properties文件,配置并启用一个测试使用的邮件服务器,准备好后,就调用该接口实现邮件发送,然后检查是否发送成功,关闭测试邮件服务器。具体代码:
public class AccountEmailServiceTest { private GreenMail greenMail; @Before public void startMailServer() throws Exception{ greenMail = new GreenMail(ServerSetup.SMTP); greenMail.setUser("1219611916@qq.com", "silence"); greenMail.start(); } @Test public void testSendMail() throws Exception{ //根据account。xml创建一个spring framework的ApplicationContext ApplicationContext ctx = new ClassPathXmlApplicationContext("account_email.xml"); //从ctx中获取需要测试的ID为accountEmailService的bean并转换成AccountEmailService接口, //针对接口的测试是最好的单元测试的实现 AccountEmailService accout = (AccountEmailService)ctx.getBean("accountEmailService"); String subject = "Test Subject"; String htmlText = "<h3>test</h3>"; accout.sendMail("1219611916@qq.com", subject, htmlText); greenMail.waitForIncomingEmail(2000, 1); Message[] mags = greenMail.getReceivedMessages(); assertEquals(1,mags.length); assertEquals(subject, mags[0].getSubject()); assertEquals(htmlText, GreenMailUtil.getBody(mags[0]).trim()); } @After public void stopMailServer() throws Exception{ greenMail.stop(); } }
实现persist模块:
该模块负责账户数据的持久化,以XML文件的形式保存账户数据,井支持账户的创建、读取、更新、删除等操作。
配置代码:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hust.silence.account</groupId> <artifactId>account-persist</artifactId> <version>0.0.1-SNAPSHOT</version> <name>account-persist</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <dom4j.version>1.6.1</dom4j.version> <springframework.version>2.5.6</springframework.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> <scope>test</test> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>${dom4j.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${springframework.version}</version> </dependency> </dependencies> <build> <testResources> <testResource> <directory>src/test/resources</directory> <filtering>true</filtering> </testResource> </testResources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
dom4j是支持XML操作的,build元素包含testresource是开启资源过滤的,在单元测试中用到。
具体代码:
public interface AccountPersistService { Account createAccount(Account account) throws AccountPersistException; Account readAccount(String id) throws AccountPersistException; Account updateAccount(Account account) throws AccountPersistException; void deleteAccount(String id) throws AccountPersistException; } public class AccountPersistServiceImpl implements AccountPersistService { private static final String ELEMENT_ROOT = "account-persist"; private static final String ELEMENT_ACCOUNTS = "accounts"; private static final String ELEMENT_ACCOUNT = "account"; private static final String ELEMENT_ACCOUNT_ID = "id"; private static final String ELEMENT_ACCOUNT_NAME = "name"; private static final String ELEMENT_ACCOUNT_EMAIL = "email"; private static final String ELEMENT_ACCOUNT_PASSWORD = "password"; private static final String ELEMENT_ACCOUNT_ACTIVATED = "activated"; private String file; private SAXReader reader = new SAXReader(); public String getFile() { return file; } public void setFile( String file ) { this.file = file; } public Account createAccount( Account account ) throws AccountPersistException { Document doc = readDocument(); Element accountsEle = doc.getRootElement().element(ELEMENT_ACCOUNTS); accountsEle.add( buildAccountElement( account ) ); writeDocument( doc ); return account; } @SuppressWarnings("unchecked") public void deleteAccount( String id ) throws AccountPersistException { Document doc = readDocument(); Element accountsEle = doc.getRootElement().element( ELEMENT_ACCOUNTS ); for ( Element accountEle : (List<Element>) accountsEle.elements() ) { if ( accountEle.elementText( ELEMENT_ACCOUNT_ID ).equals( id ) ) { accountEle.detach(); writeDocument( doc ); return; } } } @SuppressWarnings("unchecked") public Account readAccount( String id ) throws AccountPersistException { Document doc = readDocument(); Element accountsEle = doc.getRootElement().element( ELEMENT_ACCOUNTS ); for ( Element accountEle : (List<Element>) accountsEle.elements() ) { if ( accountEle.elementText( ELEMENT_ACCOUNT_ID ).equals( id ) ) { return buildAccount( accountEle ); } } return null; } public Account updateAccount( Account account ) throws AccountPersistException { if ( readAccount( account.getId() ) != null ) { deleteAccount( account.getId() ); return createAccount ( account ); } return null; } private Account buildAccount( Element element ) { Account account = new Account(); account.setId( element.elementText( ELEMENT_ACCOUNT_ID ) ); account.setName( element.elementText( ELEMENT_ACCOUNT_NAME ) ); account.setEmail( element.elementText( ELEMENT_ACCOUNT_EMAIL ) ); account.setPassword( element.elementText( ELEMENT_ACCOUNT_PASSWORD ) ); account.setActivated( ( "true".equals( element.elementText( ELEMENT_ACCOUNT_ACTIVATED ) ) ? true : false ) ); return account; } private Element buildAccountElement( Account account ) { Element element = DocumentFactory.getInstance().createElement( ELEMENT_ACCOUNT ); element.addElement( ELEMENT_ACCOUNT_ID ).setText( account.getId() ); element.addElement( ELEMENT_ACCOUNT_NAME ).setText( account.getName() ); element.addElement( ELEMENT_ACCOUNT_EMAIL ).setText( account.getEmail() ); element.addElement( ELEMENT_ACCOUNT_PASSWORD ).setText( account.getPassword() ); element.addElement( ELEMENT_ACCOUNT_ACTIVATED ).setText( account.isActivated() ? "true" : "false" ); return element; } private Document readDocument() throws AccountPersistException { File dataFile = new File( file ); if( !dataFile.exists() ) { dataFile.getParentFile().mkdirs(); Document doc = DocumentFactory.getInstance().createDocument(); Element rootEle = doc.addElement( ELEMENT_ROOT ); rootEle.addElement( ELEMENT_ACCOUNTS ); writeDocument( doc ); } try { return reader.read( new File( file ) ); } catch ( DocumentException e ) { throw new AccountPersistException( "Unable to read persist data xml", e ); } } private void writeDocument( Document doc ) throws AccountPersistException { Writer out = null; try { out = new OutputStreamWriter( new FileOutputStream( file ), "utf-8" ); XMLWriter writer = new XMLWriter( out, OutputFormat.createPrettyPrint() ); writer.write( doc ); } catch ( IOException e ) { throw new AccountPersistException( "Unable to write persist data xml", e ); } finally { try { if ( out != null) { out.close(); } } catch ( IOException e ) { throw new AccountPersistException( "Unable to close persist data xml writer", e ); } } } }
Account是一个简单的类:对变量的读取和设置
private String id;
private String name;
private String email;
private String password;
private boolean activated;
该测试用例遵守了测试接口而不测试实现这一原则:也就是说,测试代码不能引用实现类,由于测试是从接口用户的角度编写的,这样就能保证接口的用户无须知晓接口的实现细节,既保证了代码的解藕,也促进了代码的设计。
测试代码:这里只给出了读取readAccount的test
public class AccountPersistServiceTest { private AccountPersistService service; @Before public void prepare() throws Exception{ File persistDataFile = new File("target/persist-classes/persist_data.xml"); if(persistDataFile.exists()){ persistDataFile.delete(); } ApplicationContext ctx = new ClassPathXmlApplicationContext("account_persist.xml"); service = (AccountPersistService) ctx.getBean("accountPersistService"); Account account = new Account(); account.setId("ww"); account.setName("wwss"); account.setEmail("16@qq.com"); account.setPassword("####"); account.setActivated(true); service.createAccount(account); } @Test public void testReadAccount() throws Exception{ Account account = service.readAccount("ww"); assertEquals("ww",account.getId()); assertEquals("wwss",account.getName()); assertEquals("126@qq.com",account.getEmail()); assertEquals("####",account.getPassword()); assertTrue(account.isActivated()); } }
想要将上面的代码单个模块运行成功,还需要给出相应的配置文件,xml文件放在src/main/resources下,properties文件放在src/test/resources。
尤其是这里只给出了相应的两个模块的代码,有兴趣的可以继续写完成。
account_email.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="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:account_email.properties"/> </bean> <bean id="javaMailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl"> <property name="protocol" value="${email.protocol}"></property> <property name="host" value="${email.host}"></property> <property name="port" value="${email.port}"></property> <property name="username" value="${email.username }"></property> <property name="password" value="${email.password }"></property> <property name="javaMailProperties" > <props> <prop key="mail.${email.protocol}.auth">${email.auth}</prop> </props> </property> </bean> <bean id="accountEmailService" class="com.hust.silence.account.email.AccountEmailServiceImpl"> <property name="javaMailSender" ref="javaMailSender"></property> <property name="systemEmail" value="${email.systemEmail}"></property> </bean> </beans>
account_email.properties:
email.protocol=smtp email.host="127.0.0.1" email.port="25" email.username=16@qq.com email.password=#### email.auth=true email.systemEmail=16@qq.com