Java Web系列:Spring Security 基礎
Spring Security雖然比JAAS進步很大,但還是先天不足,達不到ASP.NET中的認證和授權的方便快捷。這里演示登錄、注銷、記住我的常規功能,認證上自定義提供程序避免對數據庫的依賴,授權上自定義提供程序消除從緩存加載角色信息造成的角色變更無效副作用。
1.基于java config的Spring Security基礎配置
(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 }
2.使用@PreAuthorize在Controller級別通過角色控制權限
(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 }
3.登錄和注銷功能
注銷直接使用內置功能,登錄可以自定義控制器和視圖。
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>
4.Spring Security核心對象
驗證和授權的核心的ASP.NET肯定是HttpModule,Java是Filter,這沒什么可說的,到現在兩套組合(HttpApplicaiton+HttpModule+HttpHandler)(ServletContext+Filter+Servlet)的核心概念已經熟練了。
(1)安全上下文 SecurityContext
類似.NET中 IPrincipal 的概念。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這些東西至少要對其基礎配置和核心對象有整體上的把握,至少要能快速定位開發中遇到的問題并基于對源代碼的了解能應對實際中出現的大部分技術問題才行。