AccessDecisionVoter
一句话定义:访问决策投票者,负责对单一权限检查项进行投票(
ACCESS_GRANTED/ACCESS_DENIED/ACCESS_ABSTAIN),由AccessDecisionManager汇总多个 Voter 的投票结果形成最终授权决策。
定义与作用
AccessDecisionVoter 是 Spring Security 授权投票机制的参与者。每个 Voter 只负责自己擅长的权限检查维度,通过投票表达态度。AccessDecisionManager 将 ConfigAttribute 分发给配置的 Voter 列表,收集投票结果后按策略裁决。
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1; // 赞成
int ACCESS_ABSTAIN = 0; // 弃权
int ACCESS_DENIED = -1; // 反对
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
boolean supports(ConfigAttribute attribute);
}
三种投票结果的含义:
| 结果 | 值 | 含义 |
|---|---|---|
ACCESS_GRANTED | 1 | 当前 Voter 认为权限满足 |
ACCESS_DENIED | -1 | 当前 Voter 认为权限不满足 |
ACCESS_ABSTAIN | 0 | 当前 Voter 不负责此类型权限,弃权 |
核心原理
三大内置 Voter
Spring Security 5.x 提供三个核心 Voter 实现,覆盖绝大多数场景:
各 Voter 详细解析
1. RoleVoter
public class RoleVoter implements AccessDecisionVoter<Object> {
private String rolePrefix = "ROLE_";
@Override
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED; // 一旦找到支持的 attribute,默认改为 DENY
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED; // 找到匹配权限 → 通过
}
}
}
}
return result;
}
@Override
public boolean supports(ConfigAttribute attribute) {
// 只处理 ROLE_ 前缀的 attribute
return attribute.getAttribute() != null
&& attribute.getAttribute().startsWith(rolePrefix);
}
}
RoleVoter 的投票逻辑:
- 遍历
ConfigAttribute,找到以ROLE_开头的属性 - 在
authentication.getAuthorities()中查找是否有匹配的GrantedAuthority - 找到匹配 →
ACCESS_GRANTED - 找不到匹配 →
ACCESS_DENIED - 没有
ROLE_前缀的属性 →ACCESS_ABSTAIN
2. AuthenticatedVoter
public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
public static final String IS_AUTHENTICATED_FULLY = "IS_AUTHENTICATED_FULLY";
public static final String IS_AUTHENTICATED_REMEMBERED = "IS_AUTHENTICATED_REMEMBERED";
public static final String IS_AUTHENTICATED_ANONYMOUSLY = "IS_AUTHENTICATED_ANONYMOUSLY";
@Override
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
int result = ACCESS_ABSTAIN;
for (ConfigAttribute attribute : attributes) {
if (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())) {
result = ACCESS_DENIED;
if (authentication.isAuthenticated() &&
!(authentication instanceof RememberMeAuthenticationToken) &&
!(authentication instanceof AnonymousAuthenticationToken)) {
return ACCESS_GRANTED;
}
}
// IS_AUTHENTICATED_REMEMBERED / IS_AUTHENTICATED_ANONYMOUSLY 类似...
}
return result;
}
}
认证状态层级(从高到低):
IS_AUTHENTICATED_FULLY
└── 排除 RememberMe + Anonymous
└── 通过表单/ Basic / JWT 等完整认证
IS_AUTHENTICATED_REMEMBERED
└── 包含 FULLY + RememberMe
└── 不包含 Anonymous
IS_AUTHENTICATED_ANONYMOUSLY
└── 包含 FULLY + RememberMe + Anonymous
└── 任何已识别身份(包括匿名)
3. WebExpressionVoter
解析 Spring SpEL 表达式进行投票。HttpSecurity 中的 hasRole()、hasIpAddress()、hasAnyAuthority() 等最终都转换为 SpEL 表达式,由 WebExpressionVoter 评估。
// 配置中的 SpEL 表达式
.antMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")
示例一:RoleVoter 的角色检查流程
场景说明
配置 antMatchers("/admin/**").hasRole("ADMIN"),观察 RoleVoter 如何投票。
操作前配置
@Configuration
@EnableWebSecurity
public class RoleVoterDemoConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/admin/**").hasRole("ADMIN") // 转换为 ROLE_ADMIN
.anyRequest().permitAll()
)
.formLogin(withDefaults());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN")
.and()
.withUser("user").password("{noop}user").roles("USER");
}
}
投票流程分析
结果分析
admin用户访问/admin/dashboard:RoleVoter检查authentication.getAuthorities()包含ROLE_ADMIN→ACCESS_GRANTEDAuthenticatedVoter对ROLE_ADMIN弃权 →ACCESS_ABSTAINAffirmativeBased有GRANTED→ 放行
user用户访问/admin/dashboard:RoleVoter检查authentication.getAuthorities()包含ROLE_ADMIN→ 不包含 →ACCESS_DENIEDAffirmativeBased无GRANTED有DENY→ 抛出AccessDeniedException
示例二:AuthenticatedVoter 的认证状态检查
场景说明
敏感操作(如修改密码)要求用户必须是通过完整登录(而非 RememberMe)进入的。
操作前配置
@Configuration
@EnableWebSecurity
public class AuthVoterDemoConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/change-password").authenticated() // 只要登录
.antMatchers("/sensitive/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.formLogin(withDefaults())
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400)
);
}
}
操作后配置(使用 AuthenticatedVoter 精确控制)
@Configuration
@EnableWebSecurity
public class AuthVoterDemoConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
// 修改密码:必须是完整登录(拒绝 RememberMe)
.antMatchers("/change-password")
.access("isAuthenticated() and not(rememberMe())")
// 等价写法:使用 AuthenticatedVoter 支持的常量
.antMatchers("/change-password-alt")
.access("IS_AUTHENTICATED_FULLY")
.antMatchers("/sensitive/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
.formLogin(withDefaults())
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400)
);
}
}
结果分析
| 用户状态 | 访问 /change-password | 原因 |
|---|---|---|
| 表单登录后 | ✅ 通过 | IS_AUTHENTICATED_FULLY 满足 |
| RememberMe 自动登录 | ❌ 403 | RememberMeAuthenticationToken 不满足 FULLY |
| 匿名用户 | ❌ 403 | AnonymousAuthenticationToken 不满足 FULLY |
示例三:自定义 Voter(IP 白名单 + 角色双重检查)
场景说明
管理后台要求:拥有 ADMIN 角色 且 请求来自公司内网 IP。
操作前配置
public class IpAddressVoter implements AccessDecisionVoter<FilterInvocation> {
private final List<String> allowedIpRanges;
public IpAddressVoter(List<String> allowedIpRanges) {
this.allowedIpRanges = allowedIpRanges;
}
@Override
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
String requestIp = fi.getHttpRequest().getRemoteAddr();
for (ConfigAttribute attribute : attributes) {
if (attribute.getAttribute() != null &&
attribute.getAttribute().startsWith("IP_")) {
// 找到 IP 相关配置,默认改为 DENY
if (allowedIpRanges.contains(requestIp)) {
return ACCESS_GRANTED;
}
return ACCESS_DENIED;
}
}
return ACCESS_ABSTAIN; // 没有 IP_ 配置,弃权
}
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null
&& attribute.getAttribute().startsWith("IP_");
}
}
@Configuration
public class IpVoterConfig {
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> voters = Arrays.asList(
new RoleVoter(),
new IpAddressVoter(Arrays.asList("192.168.1.100", "192.168.1.101"))
);
// 使用 UnanimousBased:两个 Voter 都必须通过
return new UnanimousBased(voters);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AccessDecisionManager accessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.accessDecisionManager(accessDecisionManager)
.antMatchers("/admin/**")
.access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
}
}
结果分析
| 用户 | IP 地址 | 访问 /admin/dashboard | 投票详情 |
|---|---|---|---|
| ADMIN | 192.168.1.100 | ✅ 通过 | RoleVoter: GRANTED + IpAddressVoter: GRANTED |
| ADMIN | 10.0.0.50 | ❌ 403 | RoleVoter: GRANTED + IpAddressVoter: DENY → UnanimousBased 拒绝 |
| USER | 192.168.1.100 | ❌ 403 | RoleVoter: DENY + IpAddressVoter: GRANTED → UnanimousBased 拒绝 |
易错场景:ROLE_ 前缀与 RoleVoter 的匹配陷阱
场景:开发者配置 hasRole("ADMIN") 同时在 UserDetails 中授予 "ADMIN"(不带 ROLE_ 前缀)。
问题:hasRole("ADMIN") 在 Spring Security 内部会被转换为 ROLE_ADMIN 的 ConfigAttribute。RoleVoter 只处理 ROLE_ 前缀的属性,因此它会检查用户权限中是否有 ROLE_ADMIN。如果 UserDetails 中只授予了 "ADMIN"(不带前缀),则 RoleVoter 投 ACCESS_DENIED。
正确做法:
// 错误:权限不带 ROLE_ 前缀
User.withUsername("admin")
.password("{noop}admin")
.authorities("ADMIN") // ❌ RoleVoter 无法匹配
.build();
// 正确:使用 roles() 方法自动添加前缀
User.withUsername("admin")
.password("{noop}admin")
.roles("ADMIN") // ✅ 内部自动转换为 ROLE_ADMIN
.build();
// 或显式使用 ROLE_ 前缀
User.withUsername("admin")
.password("{noop}admin")
.authorities("ROLE_ADMIN") // ✅ 显式带前缀
.build();
面试考点:
问:
hasRole("ADMIN")和hasAuthority("ADMIN")的区别?为什么RoleVoter找不到"ADMIN"权限? 答:hasRole("ADMIN")在构建ConfigAttribute时会自动添加ROLE_前缀,最终RoleVoter检查的是ROLE_ADMIN。hasAuthority("ADMIN")不做任何前缀处理,直接使用"ADMIN"检查。RoleVoter的supports()方法只处理以ROLE_开头的属性,因此如果UserDetails中只授予了"ADMIN"(不带前缀),RoleVoter对该属性投ACCESS_ABSTAIN而不是ACCESS_GRANTED。hasRole()必须配合.roles()或带ROLE_前缀的.authorities()使用。