Spring Security虽然比JAAS进步很大,但还是先天不足,达不到ASP.NET中的认证和授权的方便快捷。这里演示登录、注销、记住我的常规功能,认证上自定义提供程序避免对数据库的依赖,授权上自定义提供程序消除从缓存加载角色信息造成的角色变更无效副作用。
(1)使用AbstractSecurityWebApplicationInitializer集成到Spring MVC
1 public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer { 2 }
(2)使用匿名类在WebSecurityConfigurerAdapter自定义AuthenticationProvider、UserDetailsService、SecurityContextRepository。
1 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 2 @EnableWebSecurity 3 public class SecurityConfig extends WebSecurityConfigurerAdapter { 4 5 @Override 6 protected void configure(HttpSecurity http) throws Exception { 7 http.authorizeRequests().antMatchers("/account**", "/admin**").authenticated(); 8 http.formLogin().usernameParameter("userName").passwordParameter("password").loginPage("/login") 9 .loginProcessingUrl("/login").successHandler(new SavedRequestAwareAuthenticationSuccessHandler()).and() 10 .logout().logoutUrl("/logout").logoutSuccessUrl("/"); 11 http.rememberMe().rememberMeParameter("rememberMe"); 12 http.csrf().disable(); 13 http.setSharedObject(SecurityContextRepository.class, new SecurityContextRepository() { 14 15 private HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); 16 17 @Override 18 public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { 19 SecurityContext context = this.repo.loadContext(requestResponseHolder); 20 if (context != null && context.getAuthentication() != null) { 21 Membership membership = new Membership(); 22 String username = context.getAuthentication().getPrincipal().toString(); 23 String[] roles = membership.getRoles(username); 24 context.getAuthentication().getAuthorities(); 25 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, 26 "password", convertStringArrayToAuthorities(roles)); 27 context.setAuthentication(token); 28 System.out.println("check user role"); 29 } 30 return context; 31 } 32 33 @Override 34 public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { 35 this.repo.saveContext(context, request, response); 36 } 37 38 @Override 39 public boolean containsContext(HttpServletRequest request) { 40 return this.repo.containsContext(request); 41 } 42 }); 43 } 44 45 @Autowired 46 @Override 47 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 48 auth.authenticationProvider(new AuthenticationProvider() { 49 50 @Override 51 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 52 Membership membership = new Membership(); 53 String username = authentication.getName(); 54 String password = authentication.getCredentials().toString(); 55 if (membership.validateUser(username, password)) { 56 String[] roles = membership.getRoles(username); 57 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, 58 "password", convertStringArrayToAuthorities(roles)); 59 return token; 60 } 61 return null; 62 } 63 64 @Override 65 public boolean supports(Class<?> authentication) { 66 return authentication.equals(UsernamePasswordAuthenticationToken.class); 67 } 68 69 }); 70 auth.userDetailsService(new UserDetailsService() { 71 72 @Override 73 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 74 Membership membership = new Membership(); 75 if (membership.hasUser(username)) { 76 UserDetails user = new User(username, "password", 77 convertStringArrayToAuthorities(membership.getRoles(username))); 78 return user; 79 } 80 return null; 81 } 82 }); 83 } 84 85 public Collection<? extends GrantedAuthority> convertStringArrayToAuthorities(String[] roles) { 86 List<SimpleGrantedAuthority> list = new ArrayList<SimpleGrantedAuthority>(); 87 for (String role : roles) { 88 list.add(new SimpleGrantedAuthority(role)); 89 } 90 return list; 91 } 92 }
(1)使用@PreAuthorize("isAuthenticated()")注解验证登录
1 @PreAuthorize("isAuthenticated()") 2 @ResponseBody 3 @RequestMapping(value = "/account") 4 public String account() { 5 return "account"; 6 }
(2)使用@PreAuthorize("hasAuthority('admin')")注解验证角色
1 @PreAuthorize("hasAuthority('admin')") 2 @ResponseBody 3 @RequestMapping("/admin") 4 public String admin() { 5 return "admin"; 6 }
注销直接使用内置功能,登录可以自定义控制器和视图。
1 @RequestMapping(value = "/login") 2 public String login(@RequestParam(value = "error", required = false) String error, 3 @ModelAttribute("model") UserModel model, BindingResult result) { 4 if (error != null) { 5 result.rejectValue("userName", "", "Invalid username and password!"); 6 } 7 return "login"; 8 }
视图:
1 <%@ page language="java" pageEncoding="UTF-8"%> 2 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 3 <%@ taglib uri="http://www.springframework.org/tags" prefix="s"%> 4 <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%> 5 <!DOCTYPE HTML> 6 <html> 7 <head> 8 <title>Getting Started: Serving Web Content</title> 9 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 10 </head> 11 <body> 12 <h2>Login</h2> 13 <form:form modelAttribute="model"> 14 <s:bind path="*"> 15 <c:if test="${status.error}"> 16 <div id="message" class="error">Form has errors</div> 17 </c:if> 18 </s:bind> 19 <div> 20 <form:label path="userName">userName</form:label> 21 <form:input path="userName" /> 22 <form:errors path="userName" cssClass="error" /> 23 </div> 24 <div> 25 <form:label path="password">password</form:label> 26 <form:password path="password" /> 27 <form:errors path="password" cssClass="error" /> 28 </div> 29 <div> 30 <form:label path="rememberMe">rememberMe</form:label> 31 <form:checkbox path="rememberMe" /> 32 </div> 33 <input type="submit" value="submit"> 34 </form:form> 35 </html>
验证和授权的核心的ASP.NET肯定是HttpModule,Java是Filter,这没什么可说的,到现在两套组合(HttpApplicaiton+HttpModule+HttpHandler)(ServletContext+Filter+Servlet)的核心概念已经熟练了。
(1)安全上下文SecurityContext
ASP.NET中我们可以通过HttpContext.User获取IPrincipal的实例,这是通过HttpModuel(FormsAuthenticationModule)机制实现的。Spring Security中获取SecurityContext也是通过对应的Filter(SecurityContextPersistenceFilter)机制实现的。SecurityContextPersistenceFilter将功能委托给SecurityContextRepository的实例实现,因此我们在上文自定义了SecurityContextRepository实现,刷新其中的角色信息。
(2)身份认证提供程序AuthenticationProvider
AuthenticationProvider对象的authenticate方法验证并返回Authentication对象。Authentication对象是Java的Principal接口的子接口。上文自定义的AuthenticationProvider只是简单的将用户名username作为Authentication的实现类UsernamePasswordAuthenticationToken构造函数的参数传递,如果需要也可以传递其他object,调用时通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取。
(3)用户信息提供程序UserDetailsService
只有在AuthenticationProvider的实现中采用了UserDetailsService用于验证,UserDetailsService才是必须的。UserDetailsService返回一个用UserDetails对象。在AuthenticationProvider中调用UserDetailsService,将UserDetails对象作为Principal参数传递给Authentication对象,这样我们可以在Controller中通过如下语句获取UserDetails对象。事实上只需要传递用户名作为Principal参数是最实用的,多搞一个自定义UserDetails还不如自定义POJO从Service中通过用户名返回信息来的干净快捷。即使使用UserDetails对象也不一定要使用UserDetailsService,可以直接在AuthenticationProvider中构造并传递UserDetails对象。上面代码的UserDetailsService只是作为演示,实际上不会被调用。
1 UserDetails userDetails = 2 (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
(1)http://docs.spring.io/autorepo/docs/spring-security/3.2.x/guides/hellomvc.html
(2)http://docs.spring.io/spring-security/site/docs/4.0.4.CI-SNAPSHOT/reference/htmlsingle/
(3)http://docs.spring.io/spring/docs/current/spring-framework-reference/html/view.html
JAAS对核心的数据结构不关注,定义了一堆还不如没有的过程依赖,Spring Security虽然改进了易用性,但从JAAS中继承了不切实际的幻想风格。提供核心数据结构和接口就行了,非要从密码加密到用户信息获取一路跑偏到对缓存和数据库都能产生依赖,Spring基于Object的依赖注入已经不适用了,Spring Security又偏离了核心。内置的不合理,外置的很难用,整合这个词就是整一堆框架合起来才能凑合用。什么时候基于类型的依赖注入框架能取代Spring、基于数据结构的验证框架能取代Spring Security,Java Web开发的生产力估计会提高一些。到现在我还没有找到可用的基于注解的前后端统一验证框架,没有这个东西,对于快速制作演示模型简直是灾难。不管怎么说,SSH这些东西至少要对其基础配置和核心对象有整体上的把握,至少要能快速定位开发中遇到的问题并基于对源代码的了解能应对实际中出现的大部分技术问题才行。