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完美的權限管理系統。
最后可關注公眾號,一起學習。加群,每天會分享干貨,還有學習視頻領取!