歡迎關注公眾號【Ccww筆記】,原創技術文章第一時間推出
前言
權限管理系統的組件分析以及認證過程的往期文章:
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完美的權限管理系統。
各位看官還可以嗎?喜歡的話,動動手指點個贊💗,點個關注唄!!謝謝支持!
也歡迎關注公眾號【Ccww筆記】,原創技術文章第一時間推出