今天我們來聊一聊 Spring Security 中的表決機制與投票器。html
當用戶想訪問 Spring Security 中一個受保護的資源時,用戶具有一些角色,該資源的訪問也須要一些角色,在比對用戶具有的角色和資源須要的角色時,就會用到投票器和表決機制。java
當用戶想要訪問某一個資源時,投票器根據用戶的角色投出同意或者反對票,表決方式則根據投票器的結果進行表決。express
在 Spring Security 中,默認提供了三種表決機制,固然,咱們也能夠不用系統提供的表決機制和投票器,而是徹底本身來定義,這也是能夠的。ide
本文鬆哥將和你們重點介紹三種表決機制和默認的投票器。post
先來看投票器。this
在 Spring Security 中,投票器是由 AccessDecisionVoter 接口來規範的,咱們來看下 AccessDecisionVoter 接口的實現:lua
能夠看到,投票器的實現有好多種,咱們能夠選擇其中一種或多種投票器,也能夠自定義投票器,默認的投票器是 WebExpressionVoter。spa
咱們來看 AccessDecisionVoter 的定義:code
public interface AccessDecisionVoter<S> { int ACCESS_GRANTED = 1; int ACCESS_ABSTAIN = 0; int ACCESS_DENIED = -1; boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); }
我稍微解釋下:csrf
咱們來分別看下幾個投票器的實現。
RoleVoter 主要用來判斷當前請求是否具有該接口所須要的角色,咱們來看下其 vote 方法:
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if (authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN; Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { result = ACCESS_DENIED; for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } return result; }
這個方法的判斷邏輯很簡單,若是當前登陸主體爲 null,則直接返回 ACCESS_DENIED 表示拒絕訪問;不然就從當前登陸主體 authentication 中抽取出角色信息,而後和 attributes 進行對比,若是具有 attributes 中所需角色的任意一種,則返回 ACCESS_GRANTED 表示容許訪問。例如 attributes 中的角色爲 [a,b,c],當前用戶具有 a,則容許訪問,不須要三種角色同時具有。
另外還有一個須要注意的地方,就是 RoleVoter 的 supports 方法,咱們來看下:
public class RoleVoter implements AccessDecisionVoter<Object> { private String rolePrefix = "ROLE_"; public String getRolePrefix() { return rolePrefix; } public void setRolePrefix(String rolePrefix) { this.rolePrefix = rolePrefix; } public boolean supports(ConfigAttribute attribute) { if ((attribute.getAttribute() != null) && attribute.getAttribute().startsWith(getRolePrefix())) { return true; } else { return false; } } public boolean supports(Class<?> clazz) { return true; } }
能夠看到,這裏涉及到了一個 rolePrefix 前綴,這個前綴是 ROLE_
,在 supports 方法中,只有主體角色前綴是 ROLE_
,這個 supoorts 方法纔會返回 true,這個投票器纔會生效。
RoleHierarchyVoter 是 RoleVoter 的一個子類,在 RoleVoter 角色判斷的基礎上,引入了角色分層管理,也就是角色繼承,關於角色繼承,小夥伴們能夠參考鬆哥以前的文章(Spring Security 中如何讓上級擁有下級的全部權限?)。
RoleHierarchyVoter 類的 vote 方法和 RoleVoter 一致,惟一的區別在於 RoleHierarchyVoter 類重寫了 extractAuthorities 方法。
@Override Collection<? extends GrantedAuthority> extractAuthorities( Authentication authentication) { return roleHierarchy.getReachableGrantedAuthorities(authentication .getAuthorities()); }
角色分層以後,須要經過 getReachableGrantedAuthorities 方法獲取實際具有的角色,具體請參考:[Spring Security 中如何讓上級擁有下級的全部權限?]() 一文。
這是一個基於表達式權限控制的投票器,鬆哥後面專門花點時間和小夥伴們聊一聊基於表達式的權限控制,這裏咱們先不作過多展開,簡單看下它的 vote 方法:
public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) { assert authentication != null; assert fi != null; assert attributes != null; WebExpressionConfigAttribute weca = findConfigAttribute(attributes); if (weca == null) { return ACCESS_ABSTAIN; } EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi); ctx = weca.postProcess(ctx, fi); return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; }
若是你熟練使用 SpEL 的話,這段代碼應該說仍是很好理解的,不過根據個人經驗,實際工做中用到 SpEL 場景雖然有,可是很少,因此可能有不少小夥伴並不瞭解 SpEL 的用法,這個須要小夥伴們自行復習下,我也給你們推薦一篇還不錯的文章:https://www.cnblogs.com/larryzeal/p/5964621.html。
這裏代碼實際上就是根據傳入的 attributes 屬性構建 weca 對象,而後根據傳入的 authentication 參數構建 ctx 對象,最後調用 evaluateAsBoolean 方法去判斷權限是否匹配。
上面介紹這三個投票器是咱們在實際開發中使用較多的三個。
另外還有幾個比較冷門的投票器,鬆哥也稍微說下,小夥伴們瞭解下。
Jsr250Voter
處理 Jsr-250 權限註解的投票器,如 @PermitAll
,@DenyAll
等。
AuthenticatedVoter
AuthenticatedVoter 用於判斷 ConfigAttribute 上是否擁有 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 三種角色。
IS_AUTHENTICATED_FULLY 表示當前認證用戶必須是經過用戶名/密碼的方式認證的,經過 RememberMe 的方式認證無效。
IS_AUTHENTICATED_REMEMBERED 表示當前登陸用戶必須是經過 RememberMe 的方式完成認證的。
IS_AUTHENTICATED_ANONYMOUSLY 表示當前登陸用戶必須是匿名用戶。
當項目引入 RememberMe 而且想區分不一樣的認證方式時,能夠考慮這個投票器。
AbstractAclVoter
提供編寫域對象 ACL 選項的幫助方法,沒有綁定到任何特定的 ACL 系統。
PreInvocationAuthorizationAdviceVoter
使用 @PreFilter 和 @PreAuthorize 註解處理的權限,經過 PreInvocationAuthorizationAdvice 來受權。
固然,若是這些投票器不能知足需求,也能夠自定義。
一個請求不必定只有一個投票器,也可能有多個投票器,因此在投票器的基礎上咱們還須要表決機制。
表決相關的類主要是三個:
他們的繼承關係如上圖。
三個決策器都會把項目中的全部投票器調用一遍,默認使用的決策器是 AffirmativeBased。
三個決策器的區別以下:
這裏的具體判斷邏輯比較簡單,鬆哥就不貼源碼了,感興趣的小夥伴能夠本身看看。
當咱們使用基於表達式的權限控制時,像下面這樣:
http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().fullyAuthenticated()
那麼默認的投票器和決策器是在 AbstractInterceptUrlConfigurer#createDefaultAccessDecisionManager 方法中配置的:
private AccessDecisionManager createDefaultAccessDecisionManager(H http) { AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http)); return postProcess(result); } List<AccessDecisionVoter<?>> getDecisionVoters(H http) { List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>(); WebExpressionVoter expressionVoter = new WebExpressionVoter(); expressionVoter.setExpressionHandler(getExpressionHandler(http)); decisionVoters.add(expressionVoter); return decisionVoters; }
這裏就能夠看到默認的決策器和投票器,而且決策器 AffirmativeBased 對象建立好以後,還調用 postProcess 方法註冊到 Spring 容器中去了,結合鬆哥本系列前面的文章,你們知道,若是咱們想要修改該對象就很是容易了:
http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().fullyAuthenticated() .withObjectPostProcessor(new ObjectPostProcessor<AffirmativeBased>() { @Override public <O extends AffirmativeBased> O postProcess(O object) { List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>(); decisionVoters.add(new RoleHierarchyVoter(roleHierarchy())); AffirmativeBased affirmativeBased = new AffirmativeBased(decisionVoters); return (O) affirmativeBased; } }) .and() .csrf() .disable();
這裏只是給你們一個演示,正常來講咱們是不須要這樣修改的。當咱們使用不一樣的權限配置方式時,會有自動配置對應的投票器和決策器。或者咱們手動配置投票器和決策器,若是是系統配置好的,大部分狀況下並不須要咱們修改。
本文主要和小夥伴們簡單分享一下 Spring Security 中的投票器和決策器,關於受權的更多知識,鬆哥下篇文章繼續和小夥伴們細聊。