1. 簡介
在安全 Spring Web 應用程序或 REST API 的時候,Spring Security 提供的工具通常已經足夠,但有時我們需要更具體的行為。在本教程中,我們將編寫一個自定義的 AccessDecisionVoter,並演示如何使用它來抽象 Web 應用程序的授權邏輯,並將其與應用程序的業務邏輯分離。
注意: AccessDecisionVoter 從 5.8.x 版本開始被標記為棄用。建議使用 AuthorizationManager。
2. 場景
為了演示 AccessDecisionVoter 的工作原理,我們將實現一個場景,其中包含兩個用户類型,USER 和 ADMIN,在這種場景中,USER 只能在偶數分鐘訪問系統,而 ADMIN 始終被授予訪問權限。3. AccessDecisionVoter Implementations
首先,我們將描述 Spring 提供的參與最終授權決策的幾個實現,以及我們自定義的投票器。然後,我們將查看如何實現自定義投票器。
3.1. The Default AccessDecisionVoter Implementations
Spring Security 提供了幾個 AccessDecisionVoter 實現。我們將使用其中的一些作為我們安全解決方案的一部分。
讓我們看看如何以及何時這些默認投票器實現投票。
AuthenticatedVoter 將根據 Authentication 對象級別的認證進行投票——具體來説,查找完全認證的 principal、帶有 remember-me 的認證 principal,或者匿名 principal。
RoleVoter 如果配置屬性以字符串 “ROLE_” 開頭,則投票。如果確實如此,它將搜索 GrantedAuthority 列表中的 Authentication 對象。
WebExpressionVoter 允許我們使用 SpEL (Spring Expression Language) 使用 @PreAuthorize 註解來授權請求。
例如,如果使用 Java 配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
.antMatchers("/").hasAnyAuthority("ROLE_USER")
...
}
或者使用 XML 配置——我們可以使用 SpEL 在 intercept-url 標籤中,在 http 標籤中:
<http use-expressions="true">
<intercept-url pattern="/"
access="hasAuthority('ROLE_USER')"/>
...
</http>
3.2. Custom AccessDecisionVoter Implementation
現在讓我們創建一個自定義投票器——通過實現 AccessDecisionVoter 接口:
public class MinuteBasedVoter implements AccessDecisionVoter {
...
}
我們必須提供的三個方法中的第一個是 vote 方法。 vote 方法是自定義投票器中最重要的一部分,也是我們授權邏輯所在。
vote 方法可以返回三種可能的返回值:
- ACCESS_GRANTED – 投票器給出肯定答覆
- ACCESS_DENIED – 投票器給出否定答覆
- ACCESS_ABSTAIN – 投票器棄權
現在讓我們實現 vote 方法:
@Override
public int vote(
Authentication authentication, Object object, Collection collection) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(r -> "ROLE_USER".equals(r)
&& LocalDateTime.now().getMinute() % 2 != 0)
.findAny()
.map(s -> ACCESS_DENIED)
.orElseGet(() -> ACCESS_ABSTAIN);
}
在我們的 vote 方法中,我們檢查請求是否來自 USER。如果是,我們返回 ACCESS_GRANTED 如果它是偶數分鐘,否則,我們返回 ACCESS_DENIED。如果請求不來自 USER,我們棄權並返回 ACCESS_ABSTAIN。
我們 vote 方法的第二個返回值是投票器是否支持特定的配置屬性。在我們的示例中,投票器不需要任何自定義配置屬性,因此我們返回 true:
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
我們 vote 方法的第三個返回值是投票器是否可以投票給受保護的對象類型。由於我們的投票器沒有關注受保護的對象類型,因此我們返回 true:
@Override
public boolean supports(Class clazz) {
return true;
}
4. TheAccessDecisionManager
最終的授權決策由 AccessDecisionManager 處理。
AbstractAccessDecisionManager 包含一個 AccessDecisionVoter 列表 – 這些投票者獨立地投出自己的票。
有三種實現用於處理投票以覆蓋最常見的用例:
- AffirmativeBased – 如果任何 AccessDecisionVoter 返回肯定投票,則授予訪問權限
- ConsensusBased – 如果肯定投票數多於否定投票數(忽略棄權用户),則授予訪問權限
- UnanimousBased – 如果每個投票者要麼棄權,要麼返回肯定投票,則授予訪問權限
當然,您可以使用自定義決策邏輯實現自己的 AccessDecisionManager。
5. 配置
在本教程的這一部分,我們將研究基於 Java 和 XML 的方法,用於配置我們自定義的
5.1. Java 配置
讓我們為 Spring Web Security 創建一個配置類:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
...
}
並且,讓我們定義一個
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter(),
new MinuteBasedVoter());
return new UnanimousBased(decisionVoters);
}
最後,讓我們配置 Spring Security 以使用先前定義的 bean 作為默認的
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
.anyRequest()
.authenticated()
.accessDecisionManager(accessDecisionManager());
}
5.2. XML 配置
如果使用 XML 配置,您需要修改您的
首先,您需要修改
<http access-decision-manager-ref="accessDecisionManager">
<intercept-url
pattern="/**"
access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')"/>
...
</http>
接下來,添加一個用於自定義投票者的 bean:
<beans:bean
id="minuteBasedVoter"
class="com.baeldung.voter.MinuteBasedVoter"/>
然後添加一個用於
<beans:bean
id="accessDecisionManager"
class="org.springframework.security.access.vote.UnanimousBased">
<beans:constructor-arg>
<beans:list>
<beans:bean class=
"org.springframework.security.web.access.expression.WebExpressionVoter"/>
<beans:bean class=
"org.springframework.security.access.vote.AuthenticatedVoter"/>
<beans:bean class=
"org.springframework.security.access.vote.RoleVoter"/>
<beans:bean class=
"com.baeldung.voter.MinuteBasedVoter"/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
這是一個支持我們場景的
<authentication-manager>
<authentication-provider>
<user name="user" password="pass" authorities="ROLE_USER"/>
<user name="admin" password="pass" authorities="ROLE_ADMIN"/>
</user-service>
</authentication-provider>
</authentication-manager>
如果您同時使用 Java 和 XML 配置,則可以將 XML 導入到配置類中:
@Configuration
@ImportResource({"classpath:spring-security.xml"})
public class XmlSecurityConfig {
public XmlSecurityConfig() {
super();
}
}
6. 結論
在本教程中,我們探討了通過使用 AccessDecisionVoter 來自定義 Spring Web 應用程序安全設置的一種方法。我們看到了 Spring Security 提供的部分 AccessDecisionVoter 對我們的解決方案做出貢獻。然後,我們討論瞭如何實現自定義 AccessDecisionVoter。
然後,我們討論了 AccessDecisionManager 如何做出最終的授權決策,並展示瞭如何使用 Spring 提供的實現來在所有 AccessDecisionVoter 投票完畢後進行此決策。
然後,我們通過 Java 和 XML 配置了 AccessDecisionVoters 列表以及 AccessDecisionManager。
當項目本地運行時,登錄頁面可以通過以下網址訪問:
http://localhost:8082/login
USER 的憑據為 “user” 和 “pass”,而 ADMIN 的憑據為 “admin” 和 “pass”。