權限管理是一個幾乎全部後臺系統的都會涉及的一個重要組成部分,主要目的是對整個後臺管理系統進行權限的控制。常見的基於角色的訪問控制,其受權模型爲「用戶-角色-權限」,簡明的說,一個用戶擁有多個角色,一個角色擁有多個權限。其中,linux
2). 操做權限,控制你能夠在頁面上進行哪些操做(查詢、刪除、編輯等);
3).數據權限,是控制你能夠看到哪些數據。數據庫
實質是:
權限(Permission) = 資源(Resource) + 操做(Privilege)
角色(Role) = 權限的集合(a set of low-level permissions)
用戶(User) = 角色的集合(high-level roles)app
權限管理過程:ide
在實際項目中用戶數量多,逐一的爲每一個系統用戶受權,這是極其繁瑣的事,因此能夠學習linux文件管理系統同樣,設置group模式,一組有多個用戶,能夠爲用戶組受權相同的權限,簡便多了。這樣模式下:
每一個用戶的全部權限=用戶我的的權限+用戶組所用的權限
用戶組、用戶、與角色三者關係以下:源碼分析
再結合權限管理的頁面權限、操做權限,如菜單的訪問、功能模塊的操做、按鈕的操做等等,可把功能操做與資源統一管理,即讓它們直接與權限關聯起來,關係圖以下:post
FilterSecurityInterceptor doFilter()->invoke() ->AbstractSecurityInterceptor beforeInvocation() ->SecurityMetadataSource 獲取ConfigAttribute屬性信息(從數據庫或者其餘數據源地方) getAttributes() ->AccessDecisionManager() 基於AccessDecisionVoter實現受權訪問 Decide() ->AccessDecisionVoter 受AccessDecisionManager委託實現受權訪問 vote()
默認受權過程會使用這樣的工做流程,接下來來分析各個組件的功能與源碼。學習
FilterSecurityInterceptor爲受權攔截器, 在FilterSecurityInterceptor中有一個封裝了過濾鏈、request以及response的FilterInvocation對象進行操做,在FilterSecurityInterceptor,主要由invoke()調用其父類AbstractSecurityInterceptor的方法。 ui
invoke()分析:this
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() 分析以下:url
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); } }
SecurityMetadataSource是從數據庫或者其餘數據源中加載ConfigAttribute,爲了在AccessDecisionManager.decide() 最終決策中進行match。其有三個方法:
Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;//加載權限資源 Collection<ConfigAttribute> getAllConfigAttributes();//加載全部權限資源 boolean supports(Class<?> var1);
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);
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接口進行投票訪問控制決策:
且AccessDecisionVoter用三個靜態變量表示voter投票狀況:
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; } } } }
自定義組件:
自定義MyFilterSecurityInterceptor主要工做爲:
重寫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); } }
自定義MyFilterInvocationSecurityMetadataSource主要工做爲:
重寫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); }
}
自定義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; }
}
配置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筆記】,一塊兒學習。加羣,天天會分享乾貨,還有學習視頻領取!