[許可權管理系統篇] (五)-Spring security(授權過程分析)
歡迎關注公眾號【Ccww筆記】,原創技術文章第一時間推出
前言
許可權管理系統的元件分析以及認證過程的往期文章:
-
Spring security (一)架構框架-Component、Service、Filter分析
-
Spring Security(二)--WebSecurityConfigurer配置以及filter順序
-
【許可權管理系統】Spring security(三)---認證過程(原理解析,demo)
-
[許可權管理系統(四)]-spring boot +spring security簡訊認證+redis整合
1. 許可權管理相關概念
許可權管理是一個幾乎所有後臺系統的都會涉及的一個重要組成部分,主要目的是對整個後臺管理系統進行許可權的控制。常見的基於角色的訪問控制,其授權模型為“使用者-角色-許可權”,簡明的說,一個使用者擁有多個角色,一個角色擁有多個許可權。其中,
-
使用者: 不用多講,大家也知道了;
-
角色: 一個集合的概念,角色管理是確定角色具備哪些許可權的一個過程 ;
-
許可權: 1).頁面許可權,控制你可以看到哪個頁面,看不到哪個頁面; 2). 操作許可權,控制你可以在頁面上進行哪些操作(查詢、刪除、編輯等); 3).資料許可權,是控制你可以看到哪些資料。
實質是:
許可權(Permission) = 資源(Resource) + 操作(Privilege)
角色(Role) = 許可權的集合(a set of low-level permissions)
使用者(User) = 角色的集合(high-level roles)
許可權管理過程:
-
鑑權管理,即許可權判斷邏輯,如選單管理(普通業務人員登入系統後,是看不到【使用者管理】選單的)、功能許可權管理(URL訪問的管理)、行級許可權管理等
-
授權管理,即許可權分配過程,如直接對使用者授權,直接分配到使用者的許可權具有最優先級別、對使用者所屬崗位授權,使用者所屬崗位資訊可以看作是一個分組,和角色的作用一樣,但是每個使用者只能關聯一個崗位資訊等。
在實際專案中使用者數量多,逐一的為每個系統使用者授權,這是極其繁瑣的事,所以可以學習linux檔案管理系統一樣,設定group模式,一組有多個使用者,可以為使用者組授權相同的許可權,簡便多了。這樣模式下: 每個使用者的所有許可權=使用者個人的許可權+使用者組所用的許可權 使用者組、使用者、與角色三者關係如下:
再結合許可權管理的頁面許可權、操作許可權,如選單的訪問、功能模組的操作、按鈕的操作等等,可把功能操作與資源統一管理,即讓它們直接與許可權關聯起來,關係圖如下:
2. 授權過程分析
2.1 授權訪問許可權工作流程:
FilterSecurityInterceptor doFilter()->invoke() ->AbstractSecurityInterceptor beforeInvocation() ->SecurityMetadataSource 獲取ConfigAttribute屬性資訊(從資料庫或者其他資料來源地方) getAttributes() ->AccessDecisionManager() 基於AccessDecisionVoter實現授權訪問 Decide() ->AccessDecisionVoter 受AccessDecisionManager委託實現授權訪問 vote()
預設授權過程會使用這樣的工作流程,接下來來分析各個元件的功能與原始碼。
2.2 AbstractSecurityInterceptor分析
FilterSecurityInterceptor為授權攔截器, 在FilterSecurityInterceptor中有一個封裝了過濾鏈、request以及response的FilterInvocation物件進行操作,在FilterSecurityInterceptor,主要由invoke()呼叫其父類AbstractSecurityInterceptor的方法。
invoke()分析:
public void invoke(FilterInvocation fi) throws IOException, ServletException { ..... // 獲取accessDecisionManager許可權決策後結果狀態、以及許可權屬性 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
AbstractSecurityInterceptor 的授權過濾器主要方法beforeInvocation(),afterInvocation()以及authenticateIfRequired(),其最主要的方法beforeInvocation() 分析如下:
protected InterceptorStatusToken beforeInvocation(Object object) { .... //從SecurityMetadataSource的許可權屬性 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { ..... publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } //呼叫認證環節獲取authenticated(包含使用者的詳細資訊) Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //進行關鍵的一步:授權的最終決策 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } }
2.3 SecurityMetadataSource
SecurityMetadataSource是從資料庫或者其他資料來源中載入ConfigAttribute,為了在AccessDecisionManager.decide() 最終決策中進行match。其有三個方法:
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;//載入許可權資源 Collection<ConfigAttribute> getAllConfigAttributes();//載入所有許可權資源 boolean supports(Class<?> var1);
2.4 AccessDecisionManager
AccessDecisionManager被AbstractSecurityInterceptor 攔截器呼叫進行最終訪問控制決策。 而且由AuthenticationManager建立的Authentication object中的GrantedAuthority,首先被授權模組中的 AccessDecisionManager讀取使用,當複雜的GrantedAuthority,getAuthority()為null,因此需要AccessDecisionManager專門支援GrantedAuthority實現以便了解其內容。
AccessDecisionManager介面方法:
void decide(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attrs) throws AccessDeniedException; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz);
2.5 AccessDecisionVoter
AccessDecisionManager.decide()將使用AccessDecisionVoter進行投票決策。AccessDecisionVoter進行投票訪問控制決策,訪問不通過就丟擲AccessDeniedException。
AccessDecisionVoter介面方法:
int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs); boolean supports(ConfigAttribute attribute); boolean supports(Class clazz);
AccessDecisionVoter的核心方法vote() 通常是獲取Authentication的GrantedAuthority與已定義好的ConfigAttributes進行match,如果成功為投同意票,匹配不成功為拒絕票,當ConfigAttributes中無屬性時,才投棄票。
Spring Security提供了三種投票方式去實現AccessDecisionManager介面進行投票訪問控制決策:
-
ConsensusBased: 大多數voter同意訪問就授權訪問
-
AffirmativeBased: 只要一個以上voter同意訪問就授權訪問,全部
-
UnanimousBased : 只有全體同意了才授權訪問
且AccessDecisionVoter用三個靜態變量表示voter投票情況:
-
ACCESS_ABSTAIN: 棄權
-
ACCESS_DENIED: 拒絕訪問
-
ACCESS_GRANTED: 允許訪問
Note: 當所有voter都棄權時使用變數allowIfEqualGrantedDeniedDecisions來判斷,true為通過,false丟擲AccessDeniedException。
此外可自定義AccessDecisionManager實現介面,因為可能某些AccessDecisionVoter具有權重比高投票權或者某些AccessDecisionVoter具有一票否定權。AccessDecisionVoter的Spring security實現類RoleVoter和AuthenticatedVoter。RoleVoter為最為常見的AccessDecisionVoter,其為簡單的許可權表示,並以字首為ROLE_,vote匹配規則也跟上面一樣。
原始碼分析:
Public int vote(Authentication authentication,Object object,Collection<ConfigAttribute>attributes){ //使用者傳遞的authentication為null,拒絕訪問 if(authentication==null){ return ACCESS_DENIED; } int result=ACCESS_ABSTAIN; Collection<?extendsGrantedAuthority>authorities=extractAuthorities(authentication); //依次進行投票 for(ConfigAttributeattribute:attributes){ if(this.supports(attribute)){ result=ACCESS_DENIED; //Attempt to find a matching granted authority for(GrantedAuthorityauthority:authorities){ if(attribute.getAttribute().equals(authority.getAuthority())){ returnACCESS_GRANTED; } } } }
3. 案例-自定義元件
自定義元件:
-
自定義FilterSecurityInterceptor,可仿寫FilterSecurityInterceptor,實現抽象類AbstractSecurityInterceptor以及Filter介面,其主要的是把自定義的SecurityMetadataSource與自定義accessDecisionManager配置到自定義FilterSecurityInterceptor的攔截器中
-
自定義SecurityMetadataSource,實現介面FilterInvocationSecurityMetadataSource,實現從資料庫或者其他資料來源中載入ConfigAttribute(即是從資料庫或者其他資料來源中載入資源許可權)
-
自定義accessDecisionManager,可使用基於AccessDecisionVoter實現許可權認證的官方UnanimousBased
-
自定義AccessDecisionVoter
3.1 自定義MyFilterSecurityInterceptor
自定義MyFilterSecurityInterceptor主要工作為:
-
載入自定義的SecurityMetadataSource到自定義的FilterSecurityInterceptor中;
-
載入自定義的AccessDecisionManager到自定義的FilterSecurityInterceptor中;
-
重寫invoke方法
@Component public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private FilterInvocationSecurityMetadataSource securityMetadataSource; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } private void invoke(FilterInvocation fi) throws IOException, ServletException { //fi裡面有一個被攔截的url //裡面呼叫MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的所有許可權 //再呼叫MyAccessDecisionManager的decide方法來校驗使用者的許可權是否足夠 InterceptorStatusToken token = super.beforeInvocation(fi); try { //執行下一個攔截器 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } @Override public void destroy() { } @Override public Class<?> getSecureObjectClass() { return null; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } //設定自定義的FilterInvocationSecurityMetadataSource @Autowired public void setSecurityMetadataSource(MyFilterInvocationSecurityMetadataSource messageSource) { this.securityMetadataSource = messageSource; } //設定自定義的AccessDecisionManager @Override @Autowired public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) { super.setAccessDecisionManager(accessDecisionManager); } }
3.2 自定義MyFilterInvocationSecurityMetadataSource
自定義MyFilterInvocationSecurityMetadataSource主要工作為:
-
從資料來源中載入ConfigAttribute到SecurityMetadataSource資源器中
-
重寫getAttributes()載入ConfigAttribute為AccessDecisionManager.decide()授權決策做準備。
@Component public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private Map<String, Collection<ConfigAttribute>> configAttubuteMap = null; private void loadResourceDefine() { //todo 載入資料庫的所有許可權 Collection<ConfigAttribute> attributes; } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { AntPathRequestMatcher matcher; String resUrl; HttpServletRequest request = ((FilterInvocation) object).getRequest(); //1.載入許可權資源資料 if (configAttubuteMap == null) { loadResourceDefine(); } Iterator<String> iterator = configAttubuteMap.keySet().iterator(); while (iterator.hasNext()) { resUrl = iterator.next(); matcher = new AntPathRequestMatcher(resUrl); if (matcher.matches(request)) { return configAttubuteMap.get(resUrl); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
3.3 自定義MyAccessDecisionManager
自定義MyAccessDecisionManager主要工作為:
-
重寫最終授權決策decide(),自定義授權訪問策略
@Component public class MyAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { ConfigAttribute c; String needRole; if(null== configAttributes || configAttributes.size() <=0) { return; } //1.獲取已定義的好資源許可權配置 Iterator<ConfigAttribute> iterable=configAttributes.iterator(); while (iterable.hasNext()){ c=iterable.next(); needRole=c.getAttribute(); //2.依次比對使用者角色對應的資源許可權 for (GrantedAuthority grantedAuthority:authentication.getAuthorities()){ if(needRole.trim().equals(grantedAuthority.getAuthority())){ return; } } } } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
3.4 配置SecurityConfig
配置SecurityConfig主要工作為:
-
將FilterSecurityInterceptor攔截器載入WebSecurityConfig中
protected void configure(HttpSecurity http) throws Exception { http.headers().frameOptions().disable().and() //表單登入 .formLogin() .loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE) .loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL) .successHandler(authenticationSuccessHandler()) .failureHandler(authenticationFailureHandler()) .and() //應用sms認證配置 .apply(smsAuthenticationSecurityConfig) .and() //允許通過 .authorizeRequests() .antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL, SecurityConstants.APP_USER_REGISTER_URL, SecurityConstants.APP_FORM_LOGIN_INDEX_URL) .permitAll()//以上的請求都不需要認證 .and() //“記住我”配置 .rememberMe() .tokenRepository(jdbcTokenRepository())//token入庫處理類 .tokenValiditySeconds(SecurityConstants.REMEMBER_ME_VERIFY_TIME)//remember-me有效時間設定 .rememberMeParameter(SecurityConstants.REMEMBER_ME_PARAM_NAME)//請求引數名設定 .and() .csrf().disable(); //增加自定義許可權授權攔截器 http.addFilterBefore(myFilterSecurityInterceptor,FilterSecurityInterceptor.class); }
總結
Spring Security授權過程中,可以會涉主要涉及了上面上面所述的元件,其中主要的還是跟著原始碼多跑幾遍,瞭解其中的原理,才能更加流暢的碼程式碼。到此為止寫完Spring Security的認證和授權分析流程,接下來會結合前面小節,寫一個Spring security完美的許可權管理系統。
各位看官還可以嗎?喜歡的話,動動手指點個贊