Spring Security原理介紹、原始碼解析——授權過程
流程簡述
當我們成功登入,獲取access_token
,即可使用該token來訪問有許可權的介面。如上文所講,JwtAuthenticationFilter
將access_token
轉化為系統可識別的Authentication
放入安全上下文,
則來到最後一個過濾器FilterSecurityInterceptor
,該過濾則是判斷請求是否擁有許可權。
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) throws IOException,ServletException {
FilterInvocation fi = new FilterInvocation(request,response,chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException,ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null )
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling,so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(),fi.getResponse());
} else {
// first time this request being called,so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED,Boolean.TRUE);
}
// 請求之前的工作,也就是真正的許可權認證的過程
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 請求真正的controller
fi.getChain().doFilter(fi.getRequest(),fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
// 請求後的工作
super.afterInvocation(token,null);
}
}
}
複製程式碼
FilterSecurityInterceptor的主體方法依舊在doFilter中,而其中主要的方法為invoke(),大約分為三個步驟:
- beforeInvocation(fi); 驗證Context中的Authentication和目標url所需許可權是否匹配,匹配則通過,不通過則丟擲異常。
- fi.getChain().doFilter(fi.getRequest(),fi.getResponse()); 在此可以看做是,真正去訪問目標Controller。
- afterInvocation(token,null); 獲取請求後的操作。
首先來看看beforeInvocation()
beforeInvocation
abstract class AbstractSecurityInterceptor {
protected InterceptorStatusToken beforeInvocation(Object object) {
// 獲取目標url的許可權內容,這些內容可以從configuration中獲取也可以用MetadataSource中獲取
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// ……省略
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
// AccessDecisionManager用於驗證Authentication中的許可權和目標url所需許可權是否匹配,如果不匹配則丟擲AccessDeniedException異常
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,attributes);
// 下一步則是生成InterceptorStatusToken,用於AfterInvocation步驟。有興趣可以自己看
if (runAs == null) {
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(),false,object);
}
else {
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,object);
}
}
}
複製程式碼
-
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
獲取目標url所需要的許可權, 該類實現FilterInvocationSecurityMetadataSource
介面的方法。而配置url許可權也可以從WebSecurityConfig
中的configuration方法配置。 -
this.accessDecisionManager.decide(authenticated,attributes);
判斷Authentication
中的許可權目標url所需許可權是否匹配,匹配則通過;不匹配則丟擲AccessDeniedException
異常。 該方法來自AbstractAccessDecisionManager
的實現類,系統預設實現為AffirmativeBased
。 -
new InterceptorStatusToken(SecurityContextHolder.getContext(),false,object);
實現InterceptorStatusToken
並返回,包括引數中的資訊,如安全上下文、目標url所需許可權、原始的訪問請求。
之後則訪問目標Controller,獲取真正的請求內容。
afterInvocation
當我們啟用了@PreAuthorize()
、@PostAuthorize()
註解的時候則會AfterInvocationManger
,進而有以下驗證邏輯。
abstract class AbstractSecurityInterceptor {
protected Object afterInvocation(InterceptorStatusToken token,Object returnedObject) {
if (token == null) {
// public object
return returnedObject;
}
finallyInvocation(token); // continue to clean in this method for passivity
if (afterInvocationManager != null) {
// Attempt after invocation handling
try {
returnedObject = afterInvocationManager.decide(token.getSecurityContext()
.getAuthentication(),token.getSecureObject(),token
.getAttributes(),returnedObject);
}
catch (AccessDeniedException accessDeniedException) {
AuthorizationFailureEvent event = new AuthorizationFailureEvent(
token.getSecureObject(),token.getAttributes(),token
.getSecurityContext().getAuthentication(),accessDeniedException);
publishEvent(event);
throw accessDeniedException;
}
}
return returnedObject;
}
}
複製程式碼
以下程式碼則是包含AfterInvocationManager
具體的實現。
public class GlobalMethodSecurityConfiguration {
protected AfterInvocationManager afterInvocationManager() {
if (prePostEnabled()) {
AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
getExpressionHandler());
PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(
postAdvice);
List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
afterInvocationProviders.add(postInvocationAdviceProvider);
invocationProviderManager.setProviders(afterInvocationProviders);
return invocationProviderManager;
}
return null;
}
}
複製程式碼
我們可以做些什麼?
-
實現
FilterInvocationSecurityMetadataSource
,用於啟動時載入url所需的許可權,這樣就不用在configuration或者註解中將目標url許可權‘寫死’。 可以參照本例所寫的實現MyFilterInvocationSecurityMetadataSource
。 -
過載
AbstractAccessDecisionManager
,根據業務需要重寫,請求目標許可權和Authentication中許可權的驗證過程. 舉個例子,Spring Security中預設的RBAC,即,許可權認證都是根據角色判斷,固定角色只能訪問固定介面。 現在我們需要ACL許可權模型,使用者A許可權為1,使用者B許可權為5,使用者C許可權為9,介面a需要許可權6,則使用者C可以訪問, 而使用者A、B不能訪問,就是說許可權大的可以訪問許可權小的介面,如果需要改變許可權模型則過載該類即可。
總結
授權過程主要有哪些?
- 獲取請求目標所需許可權,從
FilterInvocationSecurityMetadataSource
介面的實現類獲取。 - 對比安全上下文中
Authentication
中的許可權是否匹配,在AbstractAccessDecisionManager
的實現類中比較。
連結
文章涉及到程式碼已傳到gitee上,供大家參考: gitee.com/yangzijing/…
Spring Security原始碼龐大且複雜,本人水平有限,文章難免有錯漏、表述不清之處,請大家支出。歡迎交流,希望和大家共同進步。