AccessDecisionManager 与投票器
概述
Spring Security 的授权决策基于 投票器(Voter) 模式:多个 AccessDecisionVoter 对资源访问分别投票(同意/弃权/拒绝),AccessDecisionManager 根据投票结果做出最终决策。
决策模型
| 模型 | 实现类 | 规则 |
|---|---|---|
| 一票通过 | AffirmativeBased | 只要有一个赞成票即通过(默认) |
| 多数通过 | ConsensusBased | 赞成票 > 反对票 则通过 |
| 一票否决 | UnanimousBased | 所有投票者必须赞成(或弃权)才通过 |
一、AccessDecisionVoter 接口
java
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1; // 同意
int ACCESS_ABSTAIN = 0; // 弃权
int ACCESS_DENIED = -1; // 拒绝
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}内置投票器
| 投票器 | 用途 |
|---|---|
RoleVoter | 检查 ROLE_* 前缀角色 |
RoleHierarchyVoter | 支持角色继承的投票器 |
AuthenticatedVoter | 检查认证级别(IS_AUTHENTICATED_FULLY / REMEMBERED / ANONYMOUS) |
WebExpressionVoter | 处理 hasRole() / hasAuthority() 等 SpEL 表达式 |
Jsr250Voter | 处理 @RolesAllowed 注解 |
PreInvocationAuthorizationAdviceVoter | 处理 @PreAuthorize 注解 |
二、AffirmativeBased 源码(一票通过)
java
public class AffirmativeBased extends AbstractAccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) {
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return; // 一个赞成票即放行
case AccessDecisionVoter.ACCESS_DENIED:
deny++; // 记录拒绝票
break;
default:
break; // 弃权不记录
}
}
// 没有赞成票但有拒绝票
if (deny > 0) {
throw new AccessDeniedException("Access Denied");
}
// 全部弃权:默认通过
// 如果 allowIfAllAbstainDecisions = false,也会拒绝
if (!isAllowIfAllAbstainDecisions()) {
throw new AccessDeniedException("Access Denied (all abstain)");
}
}
}三、ConsensusBased 源码(多数通过)
java
public class ConsensusBased extends AbstractAccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) {
int grant = 0, deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case ACCESS_GRANTED: grant++; break;
case ACCESS_DENIED: deny++; break;
}
}
if (grant > deny) {
return; // 赞成票多于反对票
}
if (deny > grant) {
throw new AccessDeniedException("Access Denied");
}
// 赞成 == 反对
if (grant == deny && grant > 0) {
if (isAllowIfEqualGrantedDeniedDecisions()) {
return;
}
throw new AccessDeniedException("Access Denied (equal)");
}
// 全部弃权
if (!isAllowIfAllAbstainDecisions()) {
throw new AccessDeniedException("Access Denied (all abstain)");
}
}
}四、UnanimousBased 源码(一票否决)
java
public class UnanimousBased extends AbstractAccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) {
int grant = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (result == ACCESS_DENIED) {
throw new AccessDeniedException("Access Denied");
}
if (result == ACCESS_GRANTED) {
grant++;
}
}
// 至少有一个赞成票,或全部弃权
if (grant > 0 || isAllowIfAllAbstainDecisions()) {
return;
}
throw new AccessDeniedException("Access Denied (no grant)");
}
}五、自定义投票器
5.1 示例:基于 IP 的投票器
java
@Component
public class IpAddressVoter implements AccessDecisionVoter<FilterInvocation> {
private final Set<String> whitelist = Set.of("192.168.1.0/24", "10.0.0.0/8");
@Override
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
String ip = fi.getRequest().getRemoteAddr();
if (isInWhitelist(ip)) {
return ACCESS_GRANTED;
}
// 敏感操作需要内部 IP
for (ConfigAttribute attr : attributes) {
if ("SENSITIVE_OPERATION".equals(attr.getAttribute())) {
return ACCESS_DENIED;
}
}
return ACCESS_ABSTAIN;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}5.2 示例:时间窗口投票器
java
@Component
public class TimeBasedVoter implements AccessDecisionVoter<FilterInvocation> {
@Override
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
// 系统维护时间段(凌晨 2:00~4:00)拒绝所有写操作
LocalTime now = LocalTime.now();
if (now.isAfter(LocalTime.of(2, 0)) && now.isBefore(LocalTime.of(4, 0))) {
if ("POST".equalsIgnoreCase(fi.getRequest().getMethod())
|| "PUT".equalsIgnoreCase(fi.getRequest().getMethod())
|| "DELETE".equalsIgnoreCase(fi.getRequest().getMethod())) {
return ACCESS_DENIED;
}
}
return ACCESS_ABSTAIN;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}5.3 注册自定义投票器
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private IpAddressVoter ipVoter;
@Autowired
private TimeBasedVoter timeVoter;
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> voters = Arrays.asList(
new RoleVoter(),
new AuthenticatedVoter(),
new WebExpressionVoter(),
ipVoter,
timeVoter
);
// 使用一票通过 + 自定义投票器
return new AffirmativeBased(voters);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager())
);
return http.build();
}
}六、实战:多维度投票实现分级审批
6.1 场景
企业 OA 系统需要分级审批:
| 操作级别 | 示例 | 审批要求 |
|---|---|---|
| 普通操作 | 请假 < 3 天 | 审批人有权即可 |
| 敏感操作 | 请假 > 3 天 | 审批人有权 + 非本人 + 工作日 |
| 高危操作 | 转账 > 10 万 | 审批人有权 + 二级审批 + IP 白名单 + 双因素认证 |
6.2 投票器组合
java
@Component
public class LevelBasedVoter implements AccessDecisionVoter<FilterInvocation> {
@Override
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
OperationLevel level = extractOperationLevel(fi);
switch (level) {
case NORMAL:
return ACCESS_GRANTED; // 普通操作直接通过
case SENSITIVE:
// 敏感操作需要:有权 + 非本人 + 工作日
if (!isWorkday()) return ACCESS_DENIED;
if (isSelfOperation(fi, authentication)) return ACCESS_DENIED;
return ACCESS_ABSTAIN; // 还要看角色投票器
case HIGH_RISK:
// 高危操作:需要多维度检查
if (!isInWhiteList(fi)) return ACCESS_DENIED;
if (!hasTwoFactorAuth(authentication)) return ACCESS_DENIED;
return ACCESS_ABSTAIN;
default:
return ACCESS_ABSTAIN;
}
}
}6.3 不同操作级别使用不同决策策略
java
@Configuration
public class MultiStrategyConfig {
@Bean
public AccessDecisionManager normalDecisionManager() {
// 普通操作:AffirmativeBased(一票通过)
return new AffirmativeBased(List.of(
new RoleVoter(),
new AuthenticatedVoter()
));
}
@Bean
public AccessDecisionManager sensitiveDecisionManager() {
// 敏感操作:ConsensusBased(多数一致)
return new ConsensusBased(List.of(
new RoleVoter(),
new WebExpressionVoter(),
levelBasedVoter
));
}
@Bean
public AccessDecisionManager highRiskDecisionManager() {
// 高危操作:UnanimousBased(全部通过)
return new UnanimousBased(List.of(
new RoleVoter(),
new WebExpressionVoter(),
levelBasedVoter,
twoFactorVoter,
ipAddressVoter
));
}
}七、总结
| 知识点 | 说明 |
|---|---|
AffirmativeBased | 一票通过,默认策略 |
ConsensusBased | 多数通过,赞成 > 反对 则通过 |
UnanimousBased | 一票否决,全部赞成才通过 |
RoleVoter | 检查 ROLE_* 角色 |
AuthenticatedVoter | 检查认证级别 |
WebExpressionVoter | 处理 SpEL 表达式 |
| 自定义 Voter | 实现 AccessDecisionVoter 接口,在 3 种结果中选择 |
| 投票组合 | 不同操作路径使用不同决策管理器 |
参考链接: