如今咱们将要对基于内存的UserDetailsService进行简单的扩展以使其支持用户修改密码。由于这个功能对用户名和密码存于数据库的场景更有用,因此基于o.s.s.core.userdetails.memory.InMemoryDaoImpl扩展的实现不会关注存储机制,而是关注框架对这种方式扩展的总体流程和设计。在第四章中,咱们将经过将其转移到数据库后台存储来进一步扩展咱们的基本功能。html
Spring Security框架提供的InMemoryDaoImpl内存凭证存储使用了一个简单的map来存储用户名以及关联的UserDetails。InMemoryDaoImpl使用的UserDetails实现类是o.s.s.core.userdetails.User,这个实现类将会在Spring Security API中还会看到。java
这个扩展的设计有意的进行了简化并省略了一些重要的细节,如须要用户在修改密码前提供他们的旧密码。添加这些功能将做为练习留给读者。web
咱们要首先写自定义的类来扩展基本的InMemoryDaoImpl,并提供容许用户修改密码的方法。由于用户是不可改变的对象,因此咱们copy已经存在的User对象,只是将密码替换为用户提交的值。在这里咱们定义一个接口在后面的章节中将会重用,这个接口提供了修改密码功能的一个方法:spring
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>元素。咱们须要把:app
<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:jsp
<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节详细描述了以逗号分隔的提供用户信息的语法。ide
【高效使用基于内存的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。
在第四章中,咱们将会使用基于数据库的认证信息存储并学习怎样保证数据库中的密码和其余敏感数据的安全。