本文在SpringMVC和MyBatis項目框架的基礎上整合Spring Security作為權限管理。並且完全實現一套自定義的權限管理規則。
1.權限管理
在本例中所使用的權限管理的思路如下圖所示,在系統中存在着許多帳號,同時存在着許多資源,在一個Web系統中一個典型的資源就是訪問頁面的URL,控制了這個就能夠直接控制用戶的訪問權。
由於資源非常多,直接針對資源與用戶進行設置關系會比較繁瑣,因此針對同一類或者同一組的資源打個包,稱為一組權限,這樣將權限分配給用戶的時候,一組權限中的資源也就都分配給用戶了。
這個只是一個非常簡單的權限管理方案,並且只能適用於較小的項目,因為此處給出這個只是為了便於理解自定義的Spring Security認證規則。
2.Spring Security的認證規則
要編寫自定義的認證規則,首先需要對Spring Security中的認證規則有一定的了解,下面簡單介紹下Spring Security的認證規則。
1)在Spring Security中每個URL都是一個資源,當系統啟動的時候,Spring Security會根據配置將所有的URL與訪問這個URL所需要的權限的映射數據加載到Spring Security中。
2)當一個請求訪問一個資源時,Spring Security會判斷這個URL是否需要權限驗證,如果不需要,那么直接訪問即可。
3)如果這個URL需要進行權限驗證,那么Spring Security會檢查當前請求來源所屬用戶是否登錄,如果沒有登錄,則跳轉到登錄頁面,進行登錄操作,並加載這個用戶的相關信息
4)如果登錄,那么判斷這個用戶所擁有的權限是否包含訪問這個URL所需要的權限,如果有則允許訪問
5)如果沒有權限,那么就給出相應的提示信息
3.自定義認證規則思路
根據上面一小節介紹的Spring Security認證的過程,我們相應的就能夠分析出對於這個過程我們如果要修改的話,需要進行哪些方面的改動。
3.1.自定義SecurityMetadataSource
在Spring Security中的 SecurityMetadataSource 處於上面的步驟一中,也就是用於加載URL與權限對應關系的,對於這個我們需要自己進行定義
package com.oolong.customsecurity; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.LogManager; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; /** * 加載URL與權限資源,並提供根據URL匹配權限的方法 * @author weilu2 * @date 2016年12月17日 上午11:18:52 * */ @Component public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private Map<String, List<ConfigAttribute>> resources; public CustomSecurityMetadataSource() { loadAuthorityResources(); } private void loadAuthorityResources() { // 此處在創建時從數據庫中初始化權限數據 // 將權限與資源數據整理成 Map<resource, List<Authority>> 的形式 // 注意:加載URL資源時,需要對資源進行排序,要由精確到粗略進行排序,讓精確的URL優先匹配 resources = new HashMap<>(); // 此處先偽造一些數據 List<ConfigAttribute> authorityList = new ArrayList<>(); ConfigAttribute auth = new SecurityConfig("AUTH_WELCOME"); authorityList.add(auth); resources.put("/welcome", authorityList); } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String url = ((FilterInvocation) object).getRequestUrl(); Set<String> keys = resources.keySet(); for (String k : keys) { if (url.indexOf(k) >= 0) { return resources.get(k); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { // TODO Auto-generated method stub return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
在這個類中,實現了FilterInvocationSecurityMetadataSource接口,這個接口中的 getAttributes(Object object)方法能夠根據請求的URL,獲取這個URL所需要的權限,那么我們就可以在這個類初始化的時候將所有需要的權限加載進來,然后根據我們的規則進行獲取,因此這里還需要編寫一個加載數據的方法 loadAuthorityResources(),並且在構造函數中調用。
此處加載資源為了簡化,只是隨意填充了一些數據,實際可以從數據庫中獲取。
3.2.自定義AccessDecisionManager
編寫自定義的決策管理器,決策管理器是Spring Security用來決定對於一個用戶的請求是否基於通過的中心控制。
package com.oolong.customsecurity; import java.util.Collection; import java.util.Iterator; import org.apache.log4j.LogManager; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; /** * 進行決策,根據URL獲得訪問這個資源所需要的權限,然后在與當前用戶所擁有的權限進行對比 * 如果當前用戶擁有相關權限,就直接返回,否則拋出 AccessDeniedException異常 * @author weilu2 * @date 2016年12月17日 上午11:30:40 * */ @Component public class CustomAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { LogManager.getLogger("CustomAccessDecisionManager").info("decide invoke"); if (configAttributes == null) { return; } if (configAttributes.size() <= 0) { return; } Iterator<ConfigAttribute> authorities = configAttributes.iterator(); String needAuthority = null; while(authorities.hasNext()) { ConfigAttribute authority = authorities.next(); if (authority == null || (needAuthority = authority.getAttribute()) == null) { continue; } LogManager.getLogger("CustomAccessDecisionManager").info("decide == " + needAuthority); for (GrantedAuthority ga : authentication.getAuthorities()) { if (needAuthority.equals(ga.getAuthority().trim())) { return; } } } throw new AccessDeniedException("No Authority"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
決策管理器最重要的就是這個 decide()方法,Spring Security會將當前登錄用戶信息包裝到一個 Authentication對象中,並傳入這個方法;並且調用 SecurityMetadataSource.getAttributes() 方法獲取這個URL相關的權限以參數 Collection<ConfigAttribute> 的形式傳入這個方法。
然后這個decide方法獲取到這兩個信息之后就可以進行對比決策了。如果當前用戶允許登錄,那么直接return即可。如果當前用戶不許運行登錄,則拋出一個 AccessDeniedException異常。
3.3.自定義 UserDetailsService 和 AuthenticationProvider
前面說過,要進行驗證,除了有URL與權限的映射關系,還需要有用戶的權限信息。要編寫自定義的用戶數據加載,就需要實現這兩個接口。
3.3.1.UserDetailsService
package com.oolong.customsecurity; import java.util.ArrayList; import java.util.List; import org.apache.log4j.LogManager; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import com.oolong.model.AccountInfoModel; import com.oolong.model.AuthorityModel; @Component public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LogManager.getLogger("CustomUserDetailsService").info("loadUserByUsername invoke"); // 提供到數據庫查詢該用戶的權限信息 // 關於角色和權限的轉換關系在此處處理,根據用戶與角色的關系、角色與權限的關系, // 將用戶與權限的管理整理出來 // 此處偽造一些數據 // 偽造權限 AuthorityModel authority = new AuthorityModel("AUTH_WELCOME"); List<AuthorityModel> authorities = new ArrayList<>(); authorities.add(authority); AccountInfoModel account = new AccountInfoModel("oolong", "12345"); account.setAuthorities(authorities); return account; } }
3.3.2.AuthenticationProvider
AuthenticationProvider用於包裝UserDetailsService,並將其提供給 Spring Security使用。這個接口中最重要的是實現 retrieveUser() 方法,這個請參考接口的說明進行實現,此處不再贅述。
package com.oolong.customsecurity; import org.apache.log4j.LogManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; /** * 這兩個方法用於添加額外的檢查功能,此處不需要添加,因此空着,直接實現這個抽象類即可。 * @author weilu2 * @date 2016年12月17日 下午12:20:27 * */ @Component public class CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @Autowired private UserDetailsService userDetailsService; public UserDetailsService getUserDetailService() { return this.userDetailsService; } public void setUserDetailService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { LogManager.getLogger("CustomUserDetailsAuthenticationProvider").info("retrieveUser invoke"); if (userDetailsService == null) { throw new AuthenticationServiceException(""); } UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails == null) { throw new UsernameNotFoundException(username); } if (userDetails.getUsername().equals(authentication.getPrincipal().toString()) && userDetails.getPassword().equals(authentication.getCredentials().toString())) { return userDetails; } throw new BadCredentialsException(username + authentication.getCredentials()); } }
3.4.UserDetails和GrantedAuthority
這兩個接口非常簡單,請參考源碼,此處不再贅述
4.配置
上面編寫的這些自定義的實現都有了,但是僅僅這樣是沒有用的,如何配置能夠讓它們起作用呢?
package com.oolong.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import com.oolong.customsecurity.CustomAccessDecisionManager; import com.oolong.customsecurity.CustomSecurityMetadataSource; import com.oolong.customsecurity.CustomUserDetailsAuthenticationProvider; import com.oolong.customsecurity.TempHook; @Configuration @ComponentScan(basePackageClasses={TempHook.class}) @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsAuthenticationProvider customAuthenticationProvider; @Autowired private CustomAccessDecisionManager customAccessDecisionManager; @Autowired private CustomSecurityMetadataSource customSecurityMetadataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAfter(customFilterSecurityInterceptor(), ExceptionTranslationFilter.class); http.formLogin(); } @Bean public FilterSecurityInterceptor customFilterSecurityInterceptor() { FilterSecurityInterceptor fsi = new FilterSecurityInterceptor(); fsi.setAccessDecisionManager(customAccessDecisionManager); fsi.setSecurityMetadataSource(customSecurityMetadataSource); return fsi; } }
在Spring MVC中,Spring Security是通過過濾器發揮作用的,因此我們就愛那個決策管理器與數據加載放到一個過濾器中,然后將這個過濾器插入到系統的過濾器鏈中。
此外,我們向系統中提供了一個用於檢索用戶的 AuthenticationProvicer。
還有,別忘記了,告訴系統,如果用戶沒有權限應該怎么辦,http.formLogin(),告訴Spring Security要跳轉到表單登錄頁面。
參考