现在我们将要对基于内存的UserDetailsService进行简单的扩展以使其支持用户修改密码。因为这个功能对用户名和密码存于数据库的场景更有用,所以基于o.s.s.core.userdetails.memory.InMemoryDaoImpl扩展的实现不会关注存储机制,而是关注框架对这种方式扩展的整体流程和设计。在第四章中,我们将通过将其转移到数据库后台存储来进一步扩展我们的基本功能。
Spring Security框架提供的InMemoryDaoImpl内存凭证存储使用了一个简单的map来存储用户名以及关联的UserDetails。InMemoryDaoImpl使用的UserDetails实现类是o.s.s.core.userdetails.User,这个实现类将会在Spring Security API中还会看到。
这个扩展的设计有意的进行了简化并省略了一些重要的细节,如需要用户在修改密码前提供他们的旧密码。添加这些功能将作为练习留给读者。
我们要首先写自定义的类来扩展基本的InMemoryDaoImpl,并提供允许用户修改密码的方法。因为用户是不可改变的对象,所以我们copy已经存在的User对象,只是将密码替换为用户提交的值。在这里我们定义一个接口在后面的章节中将会重用,这个接口提供了修改密码功能的一个方法:
package com.packtpub.springsecurity.security; // imports omitted public interface IChangePassword extends UserDetailsService { void changePassword(String username, String password); }
以下的代码为基于内存的用户数据存储提供了修改密码功能:
package com.packtpub.springsecurity.security; public class InMemoryChangePasswordDaoImpl extends InMemoryDaoImpl implements IChangePassword { @Override public void changePassword(String username, String password) { // get the UserDetails User userDetails = (User) getUserMap().getUser(username); // create a new UserDetails with the new password User newUserDetails = new User(userDetails.getUsername(),password, userDetails.isEnabled(), userDetails.isAccountNonExpired(), userDetails.isCredentialsNonExpired(), userDetails.isAccountNonLocked(), userDetails.getAuthorities()); // add to the map getUserMap().addUser(newUserDetails); } }
比较幸运的是,只有一点代码就能将这个简单的功能加到自定义的子类中了。我们接下来看看添加自定义UserDetailsService到pet store应用中会需要什么样的配置。
现在,我们需要重新配置Spring Security的XML配置文件以使用新的UserDetailsService实现。这可能比我们预想的要困难一些,因为<user-service>元素在Spring Security的处理过程中有特殊的处理。需要明确声明我们的自定义bean并移除我们先前声明的<user-service>元素。我们需要把:
<authentication-manager alias="authenticationManager"> <authentication-provider> <user-service id="userService"> <user authorities="ROLE_USER" name="guest" password="guest"/> </user-service> </authentication-provider> </authentication-manager>
修改为:
<authentication-provider user-service-ref="userService"/>
在这里我们看到的user-service-ref属性,引用的是一个id为userService的Spring Bean。所以在dogstore-base.xml Spring Beans配置文件中,声明了如下的bean:
<bean id="userService" class="com.packtpub.springsecurity.security. InMemoryChangePasswordDaoImpl"> <property name="userProperties"> <props> <prop key="guest">guest,ROLE_USER</prop> </props> </property> </bean>
你可能会发现,这里声明用户的语法不如<user-service>包含的<user>元素更易读。遗憾的是,<user>元素只能使用在默认的InMemoryDaoImpl实现类中,我们不能在自定义的UserDetailsService中使用了。在这里例子中,这个限制使得事情稍微复杂了一点,但是在实际中,没有人会愿意长期的将用户定义信息放在配置文件中。对于感兴趣的读者,Spring Security 3参考文档中的6.2节详细描述了以逗号分隔的提供用户信息的语法。
【高效使用基于内存的UserDetailsService。有一个常见的场景使用基于内存的UserDetailsService和硬编码的用户列表,那就是编写安全组件的单元测试。编写单元测试的人员经常编码或配置最简单的场景来测试组件的功能。使用基于内存的UserDetailsService以及定义良好的用户和GrantedAuthority值为测试编写人员提供了很可控的测试环境。】
到现在,你可以重启JBCP Pets应用,应该没有任何的配置错误报告。我们将在这个练习的最后的两步中,完成UI的功能。
我们接下来将会建立一个允许用户修改密码的简单页面。
这个页面将会通过一个简单的链接添加到“My Account”页面。首先,我们在/account/home.jsp文件中添加一个链接:
<p> Please find account functions below... </p> <ul> <li><a href="changePassword.do">Change Password</a></li> </ul>
接下来,在/account/ changePassword.jsp文件中建立“Change Password”页面本身:
<?xml version="1.0" encoding="ISO-8859-1" ?> <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <jsp:include page="../common/header.jsp"> <jsp:param name="pageTitle" value="Change Password"/> </jsp:include> <h1>Change Password</h1> <form method="post"> <label for="password">New Password</label>: <input id="password" name="password" size="20" maxlength="50" type="password"/> <br /> <input type="submit" value="Change Password"/> </form> <jsp:include page="../common/footer.jsp"/>
最后我们还要添加基于Spring MVC的AccountController来处理密码修改的请求(在前面的章节中我们没有介绍AccountController,它是账号信息主页的简单处理类)。
我们需要将对自定义UserDetailsService的应用注入到com.packtpub.springsecurity.web.controller.AccountController,这样我们就能使用修改密码的功能了。Spring的@Autowired注解实现了这一功能:
@Autowired private IChangePassword changePasswordDao;
两个接受请求的方法分别对应渲染form以及处理POST提交的form数据:
@RequestMapping(value="/account/changePassword. do",method=RequestMethod.GET) public void showChangePasswordPage() { } @RequestMapping(value="/account/changePassword. do",method=RequestMethod.POST) public String submitChangePasswordPage(@RequestParam("password") String newPassword) { Object principal = SecurityContextHolder.getContext(). getAuthentication().getPrincipal(); String username = principal.toString(); if (principal instanceof UserDetails) { username = ((UserDetails)principal).getUsername(); } changePasswordDao.changePassword(username, newPassword); SecurityContextHolder.clearContext(); return "redirect:home.do"; }
完成这些配置后,重启应用,并在站点的“My Account”下找到“Change Password”功能。
比较精细的读者可能意识到这个修改密码的form相对于现实世界的应用来说太简单了。确实,很多的修改密码实现要复杂的多,并可能包含如下的功能:
l 密码确认——通过两个文本框,确保用户输入的密码是正确的;
l 旧密码确认——通过要求用户提供要修改的旧密码,增加安全性(这对使用remember me功能的场景特别重要);
l 密码规则校验——检查密码的复杂性以及密码是否安全。
你可能也会注意到当你使用这个功能的时,会被自动退出。这是因为SecurityContextHolder.clearContext()调用导致的,它会移除用户的SecurityContext并要求他们重新认证。在练习中,我们需要给用户做出提示或者找到方法让用户免于再次认证。
在本章中,我们更细节的了解了认证用户的生命周期并对JBCP Pet Store进行了结构性的修改。我们通过添加真正的登录和退出功能,进一步的满足了安全审计的要求,并提升了用户的体验。我们也学到了如下的技术:
l 配置并使用基于Spring MVC的自定义用户登录界面;
l 配置Spring Security的退出功能;
l 使用remember me功能;
l 通过记录IP地址,实现自定义的remember me功能;
l 实现修改密码功能;
l 自定义UserDetailsService和InMemoryDaoImpl。
在第四章中,我们将会使用基于数据库的认证信息存储并学习怎样保证数据库中的密码和其他敏感数据的安全。