Java Web系列:Spring Security 基礎

剛哥521發表於2016-01-03

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

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這些東西至少要對其基礎配置和核心物件有整體上的把握,至少要能快速定位開發中遇到的問題並基於對原始碼的瞭解能應對實際中出現的大部分技術問題才行。

相關文章