1. 引言
在安全 Spring Web 應用程序或 REST API 時,Spring Security 提供的工具通常已經足夠,但有時我們需要更具體的行為。
在本教程中,我們將編寫一個自定義的 AccessDecisionVoter,並展示如何將其用於抽象 Web 應用程序的授權邏輯,並將其與應用程序的業務邏輯分離。
注意: AccessDecisionVoter 從 5.8.x 版本開始被標記為已棄用。建議使用 AuthorizationManager。
2. 場景
為了演示 AccessDecisionVoter 的工作原理,我們將實現一個包含兩個用户類型的場景:用户 和 管理員。在這種場景中,用户 只能在偶數分鐘訪問系統,而 管理員 則始終被授予訪問權限。
3. AccessDecisionVoter 實現
首先,我們將描述 Spring 提供的參與最終授權決策的實現,包括我們的自定義投票器。然後,我們將探討如何實現自定義投票器。
3.1. 默認的 AccessDecisionVoter 實現
Spring Security 提供了一些 AccessDecisionVoter 實現。我們將在此安全解決方案中使用其中一些。
讓我們看看這些默認的投票器何時以及如何投票。
AuthenticatedVoter 將根據 Authentication 對象中的身份驗證級別進行投票,具體來説,它會查找完全認證的 pricipal、帶有 remember-me 機制認證的 pricipal,或者最終的匿名 pricipal。
RoleVoter 如果配置屬性以字符串 “ROLE_” 開頭,則進行投票。如果是,它將在 GrantedAuthority 列表中查找 Authentication 對象的 Role。
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. 自定義 AccessDecisionVoter 實現
現在,讓我們創建一個自定義的投票器——通過實現 AccessDecisionVoter 接口:
public class MinuteBasedVoter implements AccessDecisionVoter {
...
}以下三種方法中,我們必須提供的第一個是 投票 方法。 投票 方法是自定義投票器中最關鍵的部分,也是我們的授權邏輯所在。
投票 方法可以返回三種可能的返回值:
- ACCESS_GRANTED – 投票器給出肯定答覆
- ACCESS_DENIED – 投票器給出否定答覆
- ACCESS_ABSTAIN – 投票器棄權
現在讓我們來實現 投票 方法:
@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。
第二個方法返回選民是否支持特定的配置屬性。在我們的示例中,選民不需要任何自定義配置屬性,因此我們返回 true:
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}第三種方法返回選民是否可以投票給已保護的對象類型。由於我們的選民並不關心已保護的對象類型,因此我們返回 true:
@Override
public boolean supports(Class clazz) {
return true;
}4. 訪問決策管理器 (AccessDecisionManager)
最終的授權決策由 AccessDecisionManager 處理。
AbstractAccessDecisionManager 包含一個 AccessDecisionVoter 列表 – 這些投票者獨立地進行投票。
為了覆蓋最常見的用例,有三種實現方式來處理投票:
- AffirmativeBased – 如果任何 AccessDecisionVoter 返回肯定投票,則授予訪問權限
- ConsensusBased – 如果肯定投票數多於否定投票數(忽略棄權用户),則授予訪問權限
- UnanimousBased – 如果所有投票者要麼棄權,要麼返回肯定投票,則授予訪問權限
當然,您可以使用自定義決策邏輯實現自己的 AccessDecisionManager。
5. 配置
在本教程的這一部分,我們將探討使用基於 Java 和 XML 的方法,配置我們自定義的 <em >AccessDecisionVoter</em> 與 <em >AccessDecisionManager</em>。
5.1. Java 配置
讓我們創建一個用於 Spring Web Security 的配置類:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
...
}
讓我們定義一個 AccessDecisionManager Bean,它使用一個 UnanimousBased Manager,並使用我們自定義的投票者列表:
@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 作為默認的 AccessDecisionManager:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
.anyRequest()
.authenticated()
.accessDecisionManager(accessDecisionManager());
}
5.2. XML 配置
如果使用 XML 配置,您需要修改您的 spring-security.xml 文件(或包含您安全設置的任何文件)。
首先,您需要修改 <http> 標籤:
<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"/>
然後添加一個 Bean 用於 AccessDecisionManager</em/>:
<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-service>
<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. 結論
在本教程中,我們探討了通過使用 <em >AccessDecisionVoter</em> 定製 Spring Web 應用程序安全的方式。我們看到了 Spring Security 提供的部分投票器對我們的解決方案的貢獻。我們還討論瞭如何實現自定義 <em >AccessDecisionVoter</em>。
然後,我們討論了 <em >AccessDecisionManager</em> 如何做出最終授權決策,並展示瞭如何使用 Spring 提供的實現來在所有投票器投票完畢後進行此決策。
接下來,我們通過 Java 和 XML 配置了多個 <em >AccessDecisionVoters</em> 以及 <em >AccessDecisionManager</em>。
當項目在本地運行時,登錄頁面可以通過以下地址訪問:
http://localhost:8082/login用户“user”和“pass”的憑據,而管理員“admin”和“pass”的憑據。