实现修改密码管理
现在我们将要对基于内存的 UserDetailsService 进行简单的扩展以使其支持用户修改密码。因为这个功能对用户名和密码存于数据库的场景更有用,所以基于 o.s.s.core.userdetails.memory.InMemoryDaoImpl 扩展的实现不会关注存储机制,而是关注框架对这种方式扩展的整体流程和设计。在第四章中,我们将通过将其转移到数据库后台存储来进一步扩展我们的基本功能。
扩展基于内存的凭证存储以支持修改密码
Spring Security 框架提供的 InMemoryDaoImpl 内存凭证存储使用了一个简单的 map 来存储用户名以及关联的 UserDetails 。 InMemoryDaoImpl 使用的 UserDetails 实现类是 o.s.s.core.userdetails.User ,这个实现类将会在 Spring Security API 中还会看到。
这个扩展的设计有意的进行了简化并省略了一些重要的细节,如需要用户在修改密码前提供他们的旧密码。添加这些功能将作为练习留给读者。
用 InMemoryChangePasswordDaoImpl 扩展 InMemoryDaoImpl
我们要首先写自定义的类来扩展基本的 InMemoryDaoImpl ,并提供允许用户修改密码的方法。因为用户是不可改变的对象,所以我们 copy 已经存在的 User 对象,只是将密码替换为用户提交的值。在这里我们定义一个接口在后面的章节中将会重用,这个接口提供了修改密码功能的一个方法:
- package com.packtpub.springsecurity.security;
-
- 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) {
-
- User userDetails =
- (User) getUserMap().getUser(username);
-
- User newUserDetails =
- new User(userDetails.getUsername(),password,
- userDetails.isEnabled(),
- userDetails.isAccountNonExpired(),
- userDetails.isCredentialsNonExpired(),
- userDetails.isAccountNonLocked(),
- userDetails.getAuthorities());
-
- getUserMap().addUser(newUserDetails);
- }
- }
比较幸运的是,只有一点代码就能将这个简单的功能加到自定义的子类中了。我们接下来看看添加自定义 UserDetailsService 到 pet store 应用中会需要什么样的配置。
配置 Spring Security 来使用 InMemoryChangePasswordDaoImpl
现在,我们需要重新配置 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 ,它是账号信息主页的简单处理类)。
为 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 。
在第四章中,我们将会使用基于数据库的认证信息存储并学习怎样保证数据库中的密码和其他敏感数据的安全。