1. 引言

大多数情况下,Spring Security提供的工具足以保护Web应用或REST API,但有时我们需要更精细的控制。本文将展示如何通过自定义AccessDecisionVoter实现灵活授权,将授权逻辑与业务逻辑解耦。

⚠️ 重要提示:Spring Security 5.8.x起,AccessDecisionVoter已被标记为废弃,推荐使用AuthorizationManager替代。但本文仍将讲解其原理,便于理解授权机制演进。

2. 场景设定

我们将实现一个特殊授权场景:

  • ADMIN角色:随时可访问系统
  • USER角色:仅在偶数分钟可访问
  • ❌ 其他情况:拒绝访问

这种基于时间的授权规则,通过自定义AccessDecisionVoter实现最为合适。

3. AccessDecisionVoter实现方案

3.1 Spring默认实现

Spring Security内置多个AccessDecisionVoter,我们将结合使用:

实现类 投票规则 典型应用场景
AuthenticatedVoter 根据认证级别投票(完全认证/记住我/匿名) 区分认证强度
RoleVoter 检查配置属性是否以"ROLE_"开头 基于角色的授权
WebExpressionVoter 支持SpEL表达式 使用@PreAuthorize注解

Java配置示例

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ...
    .antMatchers("/").hasAnyAuthority("ROLE_USER")
    ...
}

XML配置示例

<http use-expressions="true">
    <intercept-url pattern="/" 
      access="hasAuthority('ROLE_USER')"/>
    ...
</http>

3.2 自定义AccessDecisionVoter

创建MinuteBasedVoter实现授权逻辑:

public class MinuteBasedVoter implements AccessDecisionVoter {
    @Override
    public int vote(Authentication authentication, 
                    Object object, 
                    Collection<ConfigAttribute> attributes) {
        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);
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

核心逻辑解析

  1. 检查用户是否为ROLE_USER
  2. 若是且当前分钟为奇数 → 返回ACCESS_DENIED
  3. 其他情况 → 返回ACCESS_ABSTAIN(弃权)

4. AccessDecisionManager决策机制

AccessDecisionManager负责汇总所有投票结果并做出最终决策。Spring提供三种实现:

实现类 决策规则 适用场景
AffirmativeBased 任一投票者同意即授权 宽松授权策略
ConsensusBased 同意票>反对票即授权 民主决策场景
UnanimousBased 全票同意或弃权才授权 严格授权策略

关键点

  • 投票者独立决策,互不干扰
  • 可自定义实现特殊决策逻辑
  • 本文使用UnanimousBased确保严格授权

5. 配置实现

5.1 Java配置方案

@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);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
        ...
        .anyRequest()
        .authenticated()
        .accessDecisionManager(accessDecisionManager());
    }
}

配置要点

  1. 将自定义投票者加入决策链
  2. 使用UnanimousBased确保严格授权
  3. 通过accessDecisionManager()注入自定义决策器

5.2 XML配置方案

<http access-decision-manager-ref="accessDecisionManager">
  <intercept-url pattern="/**" 
    access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')"/>
  ...
</http>

<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-service>
            <user name="user" password="pass" authorities="ROLE_USER"/>
            <user name="admin" password="pass" authorities="ROLE_ADMIN"/>
        </user-service>
    </authentication-provider>
</authentication-manager>

混合配置技巧

@Configuration
@ImportResource({"classpath:spring-security.xml"})
public class XmlSecurityConfig {
    public XmlSecurityConfig() {
        super();
    }
}

6. 总结

通过自定义AccessDecisionVoter,我们实现了:

  1. ✅ 将时间敏感的授权逻辑与业务代码解耦
  2. ✅ 灵活组合多种投票策略
  3. ✅ 保持Spring Security标准配置结构

完整代码GitHub项目

测试方式

  1. 启动项目后访问:http://localhost:8082/login
  2. 使用以下凭据测试:
    • USER:user/pass(偶数分钟可访问)
    • ADMIN:admin/pass(随时可访问)

虽然AccessDecisionVoter已被废弃,但其设计思想仍值得学习。新项目建议直接使用AuthorizationManager,但理解此机制有助于深入掌握Spring Security授权原理。


原始标题:Custom Spring Security with AccessDecisionVoters

» 下一篇: Java周报,148