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;
}
}
核心逻辑解析:
- 检查用户是否为ROLE_USER
- 若是且当前分钟为奇数 → 返回
ACCESS_DENIED
- 其他情况 → 返回
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());
}
}
配置要点:
- 将自定义投票者加入决策链
- 使用UnanimousBased确保严格授权
- 通过
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,我们实现了:
- ✅ 将时间敏感的授权逻辑与业务代码解耦
- ✅ 灵活组合多种投票策略
- ✅ 保持Spring Security标准配置结构
完整代码:GitHub项目
测试方式:
- 启动项目后访问:
http://localhost:8082/login
- 使用以下凭据测试:
- USER:
user
/pass
(偶数分钟可访问) - ADMIN:
admin
/pass
(随时可访问)
- USER:
虽然AccessDecisionVoter已被废弃,但其设计思想仍值得学习。新项目建议直接使用AuthorizationManager,但理解此机制有助于深入掌握Spring Security授权原理。