RoleHierarchy
一句话定义:角色层级继承机制,定义角色之间的上下级关系,使高层级角色自动拥有低层级角色的所有权限,避免重复配置和权限膨胀。
定义与作用
在实际系统中,角色通常存在层级关系。例如 ADMIN 自然应该拥有 USER 的所有权限,而 SUPER_ADMIN 又应拥有 ADMIN 的所有权限。如果没有层级继承,必须为每个用户在数据库中存储所有权限,导致数据冗余和维护困难。
RoleHierarchy 接口让 Spring Security 在权限检查时自动展开角色继承链,将高层级角色隐式转换为低层级角色参与投票。
public interface RoleHierarchy {
// 传入用户实际拥有的权限,返回包含继承后所有权限的集合
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
}
典型层级关系:
SUPER_ADMIN > ADMIN > STAFF > USER > GUEST
拥有 SUPER_ADMIN 的用户自动拥有 ADMIN、STAFF、USER、GUEST 的所有权限。
核心原理
RoleHierarchyImpl 的工作机制
public class RoleHierarchyImpl implements RoleHierarchy {
private String hierarchy; // 层级字符串配置
@Override
public Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities) {
// 遍历传入的权限,递归查找所有可达(继承)的权限
// 如传入 ROLE_ADMIN,返回 [ROLE_ADMIN, ROLE_USER, ROLE_GUEST]
}
}
配置解析示例:
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy(
"ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST"
);
return hierarchy;
}
Spring Security 内部会将该字符串解析为有向图,然后通过可达性算法计算每个角色的继承权限。
与 RoleVoter 的集成
示例一:基础角色层级配置
场景说明
企业系统有四级角色:超级管理员、管理员、普通员工、访客。上级自动拥有下级所有权限。
操作前配置(无角色层级,权限冗余)
@Configuration
@EnableWebSecurity
public class NoHierarchyConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("super")
.password("{noop}super")
.authorities("ROLE_SUPER_ADMIN", "ROLE_ADMIN", "ROLE_STAFF", "ROLE_USER")
.and()
.withUser("admin")
.password("{noop}admin")
.authorities("ROLE_ADMIN", "ROLE_STAFF", "ROLE_USER")
.and()
.withUser("staff")
.password("{noop}staff")
.authorities("ROLE_STAFF", "ROLE_USER")
.and()
.withUser("user")
.password("{noop}user")
.authorities("ROLE_USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/super/**").hasRole("SUPER_ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/staff/**").hasRole("STAFF")
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
}
}
问题:每个用户都需要配置多个角色,新增层级时所有用户数据都要修改。
操作后配置(使用 RoleHierarchy)
@Configuration
@EnableWebSecurity
public class HierarchyConfig extends WebSecurityConfigurerAdapter {
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy(
"ROLE_SUPER_ADMIN > ROLE_ADMIN\n" +
"ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER"
);
return hierarchy;
}
// 关键:将 RoleHierarchy 注入到 AccessDecisionManager
@Bean
public AccessDecisionManager accessDecisionManager() {
RoleHierarchyVoter hierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
List<AccessDecisionVoter<?>> voters = Arrays.asList(
hierarchyVoter, // 使用支持层级展开的 Voter
new AuthenticatedVoter()
);
return new AffirmativeBased(voters);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("super")
.password("{noop}super")
.roles("SUPER_ADMIN") // 只需配置一个角色!
.and()
.withUser("admin")
.password("{noop}admin")
.roles("ADMIN")
.and()
.withUser("staff")
.password("{noop}staff")
.roles("STAFF")
.and()
.withUser("user")
.password("{noop}user")
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.accessDecisionManager(accessDecisionManager())
.antMatchers("/super/**").hasRole("SUPER_ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/staff/**").hasRole("STAFF")
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
}
}
结果分析
| 用户 | 配置角色 | 展开后权限 | 可访问路径 |
|---|---|---|---|
| super | ROLE_SUPER_ADMIN | [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_STAFF, ROLE_USER] | /super/**, /admin/**, /staff/**, /user/** |
| admin | ROLE_ADMIN | [ROLE_ADMIN, ROLE_STAFF, ROLE_USER] | /admin/**, /staff/**, /user/** |
| staff | ROLE_STAFF | [ROLE_STAFF, ROLE_USER] | /staff/**, /user/** |
| user | ROLE_USER | [ROLE_USER] | /user/** |
示例二:RoleHierarchy 在方法级别安全中的生效
场景说明
在 @PreAuthorize 注解中使用 hasRole() 时,RoleHierarchy 是否同样生效?验证方法级别安全的角色继承。
操作前配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodHierarchyConfig extends WebSecurityConfigurerAdapter {
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy()); // 关键:为方法安全注入层级
return handler;
}
@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");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(auth -> auth.anyRequest().authenticated())
.formLogin(withDefaults());
}
}
@Service
public class DocumentService {
@PreAuthorize("hasRole('USER')")
public Document readDocument(Long id) {
return documentRepository.findById(id).orElseThrow();
}
@PreAuthorize("hasRole('ADMIN')")
public void deleteDocument(Long id) {
documentRepository.deleteById(id);
}
}
结果分析
admin用户调用readDocument(1L):@PreAuthorize("hasRole('USER')")需要ROLE_USERadmin实际拥有ROLE_ADMINRoleHierarchy展开后:ROLE_ADMIN→ 包含ROLE_USERDefaultMethodSecurityExpressionHandler使用RoleHierarchy评估 → 通过
user用户调用deleteDocument(1L):@PreAuthorize("hasRole('ADMIN')")需要ROLE_ADMINuser只有ROLE_USER- 展开后仍不包含
ROLE_ADMIN→ 403
关键点:方法级别安全必须显式将 RoleHierarchy 注入 MethodSecurityExpressionHandler,否则 hasRole() 在方法安全中不会自动展开继承。
易错场景:RoleHierarchy 在 URL 级别生效但在方法级别失效
场景:开发者配置了 RoleHierarchy Bean,发现 URL 级别的 hasRole("USER") 对 ADMIN 用户有效,但 @PreAuthorize("hasRole('USER')") 对 ADMIN 用户抛出 403。
原因:Spring Security 的 URL 级别授权和方法级别授权使用两套不同的 ExpressionHandler。RoleHierarchy 不会自动传播到方法安全表达式处理器。
错误配置:
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
// 错误:没有为方法安全配置 ExpressionHandler
正确配置:
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
return hierarchy;
}
// 关键:为方法安全配置 RoleHierarchy
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy());
return handler;
}
// 为 URL 级别配置 RoleHierarchy(Spring Security 5.x 方式)
@Bean
public AccessDecisionManager accessDecisionManager() {
RoleHierarchyVoter hierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
List<AccessDecisionVoter<?>> voters = Arrays.asList(
hierarchyVoter,
new AuthenticatedVoter()
);
return new AffirmativeBased(voters);
}
面试考点:
问:配置了
RoleHierarchy后,为什么@PreAuthorize("hasRole('USER')")对ADMIN用户不生效? 答:RoleHierarchy默认只注入到 URL 级别的授权体系中(通过RoleHierarchyVoter)。方法级别安全使用MethodSecurityExpressionHandler解析 SpEL 表达式,需要显式创建DefaultMethodSecurityExpressionHandler并调用setRoleHierarchy(roleHierarchy())注入层级。URL 级别和方法级别是两个独立的授权管道,配置不会自动传播。
角色层级配置速查
| 配置位置 | 需要注入 RoleHierarchy 的组件 | 代码示例 |
|---|---|---|
| URL 级别 | RoleHierarchyVoter + AccessDecisionManager | new RoleHierarchyVoter(roleHierarchy) |
| 方法级别 | MethodSecurityExpressionHandler | handler.setRoleHierarchy(roleHierarchy) |
| 两者兼顾 | 分别配置上述两套 | 见上面正确配置 |