Spring Security 自定義訪問決策者

Spring Security
Remote
1
10:36 PM · Nov 29 ,2025

1. 簡介在安全 Spring Web 應用程序或 REST API 的時候,Spring Security 提供的工具通常已經足夠,但有時我們需要更具體的行為。

在本教程中,我們將編寫一個自定義的 AccessDecisionVoter,並演示如何使用它來抽象 Web 應用程序的授權邏輯,並將其與應用程序的業務邏輯分離。

注意: AccessDecisionVoter 從 5.8.x 版本開始被標記為棄用。建議使用 AuthorizationManager

2. 場景為了演示 AccessDecisionVoter 的工作原理,我們將實現一個場景,其中包含兩個用户類型,USERADMIN,在這種場景中,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 的方法,用於配置我們自定義的 AccessDecisionVoterAccessDecisionManager

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"/>

然後添加一個用於 AccessDecisionManager 的 bean:

<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-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”。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.