Spring Security-授權流程
整個認證的過程其實一直在圍繞圖中過濾鏈的綠色部分,而我們今天要說的動態鑑權主要是圍繞其橙色部分,也就是圖上標的:FilterSecurityInterceptor
1.基本流程
1.1FilterSecurityInterceptor
想知道怎麼動態鑑權首先我們要搞明白SpringSecurity的鑑權邏輯,從上圖中我們也可以看出:FilterSecurityInterceptor
是這個過濾鏈的最後一環,而認證之後就是鑑權,所以我們的FilterSecurityInterceptor
主要是負責鑑權這部分。
一個請求完成了認證,且沒有丟擲異常之後就會到達FilterSecurityInterceptor
FilterSecurityInterceptor
。
我們先來看看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);
}
}
上文程式碼可以看出FilterSecurityInterceptor
是實現了抽象類AbstractSecurityInterceptor
的一個實現類,這個AbstractSecurityInterceptor
中預先寫好了一段很重要的程式碼(後面會說到)。
FilterSecurityInterceptor
的主要方法是doFilter
方法,過濾器的特性大家應該都知道,請求過來之後會執行這個doFilter
方法,FilterSecurityInterceptor
的doFilter
方法出奇的簡單,總共只有兩行:
第一行是建立了一個FilterInvocation
物件,這個FilterInvocation
第二行就呼叫了自身的invoke
方法,並將FilterInvocation
物件傳入。
所以我們主要邏輯肯定是在這個invoke
方法裡面了,我們來開啟看看:
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 {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
invoke
方法中只有一個if-else,一般都是不滿足if中的那三個條件的,然後執行邏輯會來到else。
else的程式碼也可以概括為兩部分:
- 呼叫了
super.beforeInvocation(fi)
。 - 呼叫完之後過濾器繼續往下走。
第二步可以不看,每個過濾器都有這麼一步,所以我們主要看super.beforeInvocation(fi)
,前文我已經說過,FilterSecurityInterceptor
實現了抽象類AbstractSecurityInterceptor
,
所以這個裡super其實指的就是AbstractSecurityInterceptor
,
那這段程式碼其實呼叫了AbstractSecurityInterceptor.beforeInvocation(fi)
,
前文我說過AbstractSecurityInterceptor
中有一段很重要的程式碼就是這一段,
那我們繼續來看這個beforeInvocation(fi)
方法的原始碼:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
try {
// 鑑權需要呼叫的介面
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
}
原始碼較長,這裡我精簡了中間的一部分,這段程式碼大致可以分為三步:
- 拿到了一個
Collection<ConfigAttribute>
物件,這個物件是一個List,其實裡面就是我們在配置檔案中配置的過濾規則。 - 拿到了
Authentication
,這裡是呼叫authenticateIfRequired
方法拿到了,其實裡面還是通過SecurityContextHolder
拿到的,上一篇文章我講過如何拿取。 - 呼叫了
accessDecisionManager.decide(authenticated, object, attributes)
,前兩步都是對decide
方法做引數的準備,第三步才是正式去到鑑權的邏輯,既然這裡面才是真正鑑權的邏輯,那也就是說鑑權其實是accessDecisionManager
在做。
1.2. AccessDecisionManager
前面通過原始碼我們看到了鑑權的真正處理者:AccessDecisionManager
,是不是覺得一層接著一層,就像套娃一樣,別急,下面還有。先來看看原始碼介面定義:
public interface AccessDecisionManager {
// 主要鑑權方法
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
AccessDecisionManager
是一個介面,它聲明瞭三個方法,除了第一個鑑權方法以外,還有兩個是輔助性的方法,其作用都是甄別decide
方法中引數的有效性。
那既然是一個介面,上文中所呼叫的肯定是他的實現類了,我們來看看這個介面的結構樹:
從圖中我們可以看到它主要有三個實現類,分別代表了三種不同的鑑權邏輯:
- AffirmativeBased:一票通過,只要有一票通過就算通過,預設是它。
- UnanimousBased:一票反對,只要有一票反對就不能通過。
- ConsensusBased:少數票服從多數票。
這裡的表述為什麼要用票呢?因為在實現類裡面採用了委託的形式,將請求委託給投票器,每個投票器拿著這個請求根據自身的邏輯來計算出能不能通過然後進行投票,所以會有上面的表述。
也就是說這三個實現類,其實還不是真正判斷請求能不能通過的類,真正判斷請求是否通過的是投票器,然後實現類把投票器的結果綜合起來來決定到底能不能通過。
剛剛已經說過,實現類把投票器的結果綜合起來進行決定,也就是說投票器可以放入多個,每個實現類裡的投票器數量取決於構造的時候放入了多少投票器,我們可以看看預設的AffirmativeBased
的原始碼。
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}
// 拿到所有的投票器,迴圈遍歷進行投票
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}
AffirmativeBased
的構造是傳入投票器List,其主要鑑權邏輯交給投票器去判斷,投票器返回不同的數字代表不同的結果,然後AffirmativeBased
根據自身一票通過的策略決定放行還是丟擲異常。
AffirmativeBased
預設傳入的構造器只有一個->WebExpressionVoter
,這個構造器會根據你在配置檔案中的配置進行邏輯處理得出投票結果。
所以SpringSecurity
預設的鑑權邏輯就是根據配置檔案中的配置進行鑑權,這是符合我們現有認知的。
1.3. ✍動態鑑權實現
通過上面一步步的講述,我想你也應該理解了SpringSecurity
到底是什麼實現鑑權的,那我們想要做到動態的給予某個角色不同的訪問許可權應該怎麼做呢?
既然是動態鑑權了,那我們的許可權URI肯定是放在資料庫中了,我們要做的就是實時的在資料庫中去讀取不同角色對應的許可權然後與當前登入的使用者做個比較。
1.重寫AbstractSecurityInterceptor
1)doFilter:請求過來之後會執行這個doFilter方法,FilterSecurityInterceptor的doFilter方法出奇的簡單,總共只有兩行:
第一行是建立了一個FilterInvocation物件,這個FilterInvocation物件你可以當作它封裝了request,它的主要工作就是拿請求裡面的資訊,比如請求的URI。
第二行就呼叫了自身的invoke方法,並將FilterInvocation物件傳入
以上是原始碼說明,自定義的部分主要判斷
a)請求的url是否攜帶token
b)token是否有效
2)invoke()主要邏輯實現,該方法的重點是 InterceptorStatusToken token = super.beforeInvocation(fi),實現鑑權。大致分三步:
1)拿到了一個Collection<ConfigAttribute>物件,這個物件是一個List,其實裡面就是我們在配置檔案中配置的過濾規則。
2)拿到了Authentication,這裡是呼叫authenticateIfRequired方法拿到了,其實裡面還是通過SecurityContextHolder拿到的,上一篇文章我講過如何拿取。
3)呼叫了accessDecisionManager.decide(authenticated, object, attributes),前兩步都是對decide方法做引數的準備,第三步才是正式去到鑑權的邏輯,既然這裡面才是真正鑑權的邏輯,那也就是說鑑權其實是accessDecisionManager在做。
2.重寫FilterInvocationSecurityMetadataSource
從資料庫中獲取許可權
3.重寫AccessDecisionManager
許可權對比
總結:
- 通過 obtainSecurityMetadataSource().getAttributes() 獲取 當前訪問地址所需許可權資訊
- 通過 authenticateIfRequired() 獲取當前訪問使用者的許可權資訊
- 通過 accessDecisionManager.decide() 使用 投票機制判權,判權失敗直接丟擲 AccessDeniedException 異常