【Spring Security】授权(四)
Spring Security
- 授权(Authorization)
- 基于角色与基于权限的设计模式
- 基本概念
- 理解角色与权限
- 基于角色(RBAC)的访问控制
- 基于权限(PBAC)的访问控制
- 动态权限设计
- 动态刷新权限表
授权(Authorization)
基于角色与基于权限的设计模式
基本概念
本质定义:宏观身份 vs 微观操作
角色和权限是 Spring Security 权限控制的两个核心概念,但定位截然不同:
- 角色(Role):用户的宏观身份标识
- 核心定位:对用户群体的「身份归类」,是一组权限的集合,描述用户在系统中的整体角色(如管理员、普通用户)。
- 典型场景:系统中需要区分不同身份的用户(如管理员可操作所有功能,普通用户仅能操作个人数据)。
- 示例:
ROLE_ADMIN(管理员角色)、ROLE_USER(普通用户角色)、ROLE_MANAGER(经理角色)。
- 权限(Authority):用户的微观操作许可
- 核心定位:对具体业务操作的「细粒度许可」,描述用户可执行的具体动作(如查看用户、删除订单)。
- 典型场景:系统需要精准控制用户的操作范围(如普通用户仅能「查看」订单,不能「删除」订单)。
- 示例:
user:read(查看用户)、user:delete(删除用户)、order:create(创建订单)。
技术底层:统一实现,差异仅在约定
在 Spring Security 内部,角色和权限本质上没有区别,均通过 GrantedAuthority 接口实现,核心差异仅在于「是否遵循 ROLE_ 前缀约定」。
- 核心接口:
GrantedAuthority
Spring Security 中所有权限相关的信息都封装为GrantedAuthority实例,该接口仅有一个方法:
角色和权限的实例化均使用其实现类public interface GrantedAuthority {// 返回权限字符串(角色或权限的核心标识)String getAuthority(); }SimpleGrantedAuthority:// 角色实例(遵循 ROLE_ 前缀约定) GrantedAuthority adminRole = new SimpleGrantedAuthority("ROLE_ADMIN"); // 权限实例(无固定前缀,通常为「资源:操作」格式) GrantedAuthority userReadPerm = new SimpleGrantedAuthority("user:read"); - 用户与权限的关联:
UserDetails
用户的所有角色 / 权限都通过UserDetails接口的getAuthorities()方法返回,该方法返回Collection<? extends GrantedAuthority>集合,即用户的「权限集合」:
示例:自定义用户详情类返回角色和权限集合:public interface UserDetails {Collection<? extends GrantedAuthority> getAuthorities();// 其他方法:getUsername()、getPassword() 等 }public class CustomUserDetails implements UserDetails {private final User user;private final List<GrantedAuthority> authorities;public CustomUserDetails(User user, List<String> permCodes) {this.user = user;// 角色(ROLE_ 前缀)+ 权限(自定义格式)统一封装为 GrantedAuthoritythis.authorities = new ArrayList<>();// 添加角色this.authorities.add(new SimpleGrantedAuthority(user.getRole()));// 添加权限permCodes.forEach(perm -> authorities.add(new SimpleGrantedAuthority(perm)));}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;} } - 授权校验的本质:字符串匹配
Spring Security 的授权校验核心是「权限集合中是否包含目标字符串」,角色和权限的校验差异仅在于目标字符串的格式:校验方法 目标字符串格式 底层匹配逻辑 hasRole("ADMIN")自动拼接 ROLE_前缀 →ROLE_ADMIN检查用户权限集合中是否包含 ROLE_ADMINhasAuthority("user:read")直接使用传入字符串 → user:read检查用户权限集合中是否包含 user:readhasAnyRole("ADMIN", "USER")拼接为 ROLE_ADMIN、ROLE_USER检查用户权限集合中是否包含任意一个角色字符串 hasAnyAuthority("user:read", "user:write")直接使用传入字符串 检查用户权限集合中是否包含任意一个权限字符串 关键结论: hasRole("ADMIN")等价于hasAuthority("ROLE_ADMIN"),前者是后者的语法糖,简化了角色校验的写法。
数据库设计:从概念到存储的映射
在实际项目中,角色和权限的存储需通过数据库表结构体现,核心遵循「用户 - 角色 - 权限」的关联关系(RBAC 基础模型)。
- 核心表结构设计
表名 核心字段 作用 sys_user(用户表)id、username、password存储用户基础信息 sys_role(角色表)id、role_name存储角色信息(角色名需遵循 ROLE_前缀)sys_permission(权限表)id、perm_code、description存储权限信息(权限码通常为「资源:操作」格式) sys_user_role(用户 - 角色关联表)user_id、role_id建立用户与角色的多对多关系 sys_role_permission(角色 - 权限关联表)role_id、perm_id建立角色与权限的多对多关系 - 表数据示例
- sys_user:
id username password 1 admin 123456 2 alice 123456 - sys_role:
id role_name 1 ROLE_ADMIN 2 ROLE_USER - sys_permission:
id perm_code description 1 user:read 查看用户 2 user:delete 删除用户 - sys_user_role:
user_id role_id 1 1 2 2 - sys_role_permission:
role_id perm_id 1 1 1 2 2 1
- sys_user:
- 数据查询逻辑
当用户登录时,系统通过以下逻辑加载其角色和权限:- 根据用户名查询
sys_user得到用户信息; - 通过
sys_user_role关联查询用户拥有的角色; - 通过
sys_role_permission关联查询每个角色对应的权限; - 将角色(
role_name)和权限(perm_code)封装为GrantedAuthority集合,存入UserDetails。
- 根据用户名查询
核心差异对比
为了更清晰地梳理两者的区别,整理如下对比表:
| 维度 | 角色(Role) | 权限(Authority) |
|---|---|---|
| 核心定位 | 宏观身份标识(用户归类) | 微观操作许可(具体动作) |
| 命名约定 | 必须以 ROLE_ 为前缀(否则 hasRole 无法识别) | 无固定前缀,推荐「资源:操作」格式(如 user:read) |
| 粒度级别 | 较粗(一组权限的集合) | 极细(单个操作的许可) |
| 适用场景 | 简单系统的身份区分(如管理员 vs 普通用户) | 复杂系统的细粒度操作控制(如查看 / 删除 / 编辑) |
| 底层实现 | SimpleGrantedAuthority(字符串含 ROLE_ 前缀) | SimpleGrantedAuthority(字符串自定义格式) |
| 校验方法 | hasRole()、hasAnyRole() | hasAuthority()、hasAnyAuthority() |
注意事项
- 角色前缀遗漏导致校验失败
- 问题:配置
hasRole("ADMIN")后,用户虽拥有ADMIN权限字符串,但校验仍失败; - 原因:
hasRole会自动拼接ROLE_前缀,实际校验的是ROLE_ADMIN,而用户权限字符串为ADMIN(无前缀); - 解决方案:
- 角色存储时添加
ROLE_前缀(如ROLE_ADMIN); - 若不想使用前缀,改用
hasAuthority("ADMIN")直接校验权限字符串。
- 角色存储时添加
- 问题:配置
- 权限命名不规范导致维护困难
- 问题:权限字符串格式混乱(如
readUser、delete_user、ORDERCREATE),后期难以维护; - 解决方案:统一权限命名规范,推荐「资源:操作」格式(如
user:read、order:delete),清晰区分资源和操作类型。
- 问题:权限字符串格式混乱(如
- 混淆角色与权限的适用场景
- 问题:在复杂系统中仅使用角色控制权限,导致权限过度授予(如普通用户需「编辑自己的资料」,却被分配了包含所有用户编辑权限的角色);
- 解决方案:复杂系统优先使用权限进行细粒度控制,角色仅作为权限的集合载体,避免直接通过角色控制具体操作。
理解角色与权限
核心桥梁:GrantedAuthority 接口的统一抽象
Spring Security 对角色和权限的理解,完全基于 GrantedAuthority 接口 ——它不区分 “角色” 和 “权限” 的语义,只认接口返回的字符串。所有与权限相关的判断,本质都是对 GrantedAuthority.getAuthority() 返回值的字符串匹配。
- 接口的核心作用
GrantedAuthority是 Spring Security 权限体系的 “最小单元”,其唯一职责是提供一个权限标识字符串。无论是角色(如ROLE_ADMIN)还是权限(如user:read),在 Spring Security 眼中都是 “一串带标识的字符串”,区别仅在于开发者赋予的语义和约定(如ROLE_前缀)。 - 实例化与存储
所有角色和权限都通过SimpleGrantedAuthority(GrantedAuthority的默认实现)实例化,最终存储在UserDetails的权限集合中:// 1. 实例化角色(遵循 ROLE_ 前缀约定) GrantedAuthority adminRole = new SimpleGrantedAuthority("ROLE_ADMIN"); // 2. 实例化权限(自定义格式) GrantedAuthority userReadPerm = new SimpleGrantedAuthority("user:read");// 3. 存储到 UserDetails 的权限集合 List<GrantedAuthority> authorities = Arrays.asList(adminRole, userReadPerm); UserDetails userDetails = User.withUsername("admin").password("123456").authorities(authorities) // 角色和权限统一存入.build();UserDetails的权限集合中,角色和权限是 “平级存储” 的,没有语义上的分层,仅通过字符串格式区分。
授权校验的底层逻辑
Spring Security 的授权校验(如 hasRole、hasAuthority),本质是 “检查用户的权限集合中,是否包含目标字符串”。不同校验方法的差异,仅在于 “目标字符串是否需要加工”。
- 角色校验:
hasRole与hasAnyRole的逻辑
hasRole方法会自动为传入的角色名拼接ROLE_前缀,再去用户的权限集合中匹配字符串:- 示例:
hasRole("ADMIN")的校验流程:- 拼接前缀:
"ADMIN"→"ROLE_ADMIN"; - 遍历用户的
GrantedAuthority集合,检查是否有getAuthority()返回"ROLE_ADMIN"的实例; - 若存在,校验通过;否则,校验失败。
- 拼接前缀:
hasAnyRole逻辑类似,只是支持多个角色名,只要有一个拼接后的字符串匹配,即通过校验:// 校验逻辑:用户权限集合中是否包含 "ROLE_ADMIN" 或 "ROLE_USER" hasAnyRole("ADMIN", "USER")
- 示例:
- 权限校验:
hasAuthority与hasAnyAuthority的逻辑
hasAuthority方法直接使用传入的字符串,不做任何加工,直接去用户的权限集合中匹配:- 示例:
hasAuthority("user:read")的校验流程:- 直接使用目标字符串:
"user:read"; - 遍历用户的
GrantedAuthority集合,检查是否有getAuthority()返回"user:read"的实例; - 若存在,校验通过;否则,校验失败。
- 直接使用目标字符串:
hasAnyAuthority支持多个权限字符串,只要有一个匹配,即通过校验:// 校验逻辑:用户权限集合中是否包含 "user:read" 或 "user:delete" hasAnyAuthority("user:read", "user:delete")
- 示例:
- 两种校验的等价关系
通过底层逻辑可推导出:角色校验是权限校验的 “语法糖”,两者可互相转换:角色校验方法 等价的权限校验方法 本质匹配的字符串 hasRole("ADMIN")hasAuthority("ROLE_ADMIN")"ROLE_ADMIN"hasAnyRole("A", "B")hasAnyAuthority("ROLE_A", "ROLE_B")"ROLE_A"或"ROLE_B"
从代码看 Spring Security 的理解逻辑
通过 Web 层授权和方法级授权的配置示例,可直观看到 Spring Security 对角色与权限的处理差异:
- Web 层授权配置
http.authorizeHttpRequests(auth -> auth// 角色校验:匹配 "ROLE_ADMIN".requestMatchers("/admin/**").hasRole("ADMIN")// 权限校验:匹配 "user:read".requestMatchers(HttpMethod.GET, "/api/users").hasAuthority("user:read")// 多角色校验:匹配 "ROLE_USER" 或 "ROLE_ADMIN".requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")// 多权限校验:匹配 "order:create" 或 "order:update".requestMatchers(HttpMethod.POST, "/api/orders").hasAnyAuthority("order:create", "order:update").anyRequest().authenticated() ); - 方法级授权配置
@Service public class UserService {// 角色校验:仅 "ROLE_ADMIN" 可调用@PreAuthorize("hasRole('ADMIN')")public void deleteUser(Long id) { ... }// 权限校验:仅 "user:read" 可调用@PreAuthorize("hasAuthority('user:read')")public UserDTO getUser(Long id) { ... }// 混合校验:"ROLE_ADMIN" 或("ROLE_USER" 且 "user:update")@PreAuthorize("hasRole('ADMIN') or (hasRole('USER') and hasAuthority('user:update'))")public void updateUser(Long id, UserDTO dto) { ... } }
注意事项
Spring Security 对角色与权限的 “无差别对待”,容易导致开发者因 “语义约定” 与 “代码实现” 不符而踩坑,需重点关注以下两点:
ROLE_前缀是 “约定” 而非 “强制”- Spring Security 仅在
hasRole/hasAnyRole方法中会自动拼接ROLE_前缀,若角色字符串未加前缀,这些方法会校验失败; - 若数据库中存储的角色是
ADMIN(无前缀),使用hasRole("ADMIN")会匹配ROLE_ADMIN,导致校验失败,此时有两种解决方案:- 存储角色时添加
ROLE_前缀(推荐,符合约定); - 改用
hasAuthority("ADMIN")直接校验(不推荐,破坏角色语义)。
- 存储角色时添加
- Spring Security 仅在
- 权限字符串格式需 “自定义约定”
- Spring Security 对权限字符串的格式无强制要求(如
user:read、READ_USER、user.read均可),但需在项目中统一格式,避免后期维护混乱; - 推荐格式:
资源:操作(如user:read、order:delete),清晰区分 “操作对象” 和 “操作类型”,便于扩展和理解。
- Spring Security 对权限字符串的格式无强制要求(如
hasRole 的实现逻辑
通过 Spring Security 源码(SecurityExpressionRoot 类)可进一步验证 hasRole 的前缀拼接逻辑:
public class SecurityExpressionRoot implements SecurityExpressionOperations {// 角色前缀,默认是 "ROLE_"private String defaultRolePrefix = "ROLE_";// hasRole 方法实现public boolean hasRole(String role) {// 拼接前缀后调用 hasAuthorityreturn hasAuthority(defaultRolePrefix + role);}// hasAuthority 方法实现public boolean hasAuthority(String authority) {// 检查用户权限集合中是否包含目标字符串return getAuthoritySet().contains(authority);}// 获取用户的权限字符串集合private Set<String> getAuthoritySet() {if (authoritySet == null) {authoritySet = new HashSet<>();for (GrantedAuthority authority : authentication.getAuthorities()) {authoritySet.add(authority.getAuthority());}}return authoritySet;}
}
hasRole 本质是调用 hasAuthority,只是多了一步 “前缀拼接”,彻底印证了 “角色校验是权限校验的语法糖” 这一结论。
基于角色(RBAC)的访问控制
基于角色的访问控制(Role-Based Access Control,简称 RBAC)是 Spring Security 中最基础、最常用的权限设计模式。它通过「用户关联角色,角色关联资源」的层级关系,简化权限分配与管理,非常适合中小型系统或权限逻辑较稳定的场景。
核心原理:角色作为权限的 “集合载体”
RBAC 的核心思想是用 “角色” 作为用户与资源之间的中间层:
- 不直接给用户分配权限,而是先定义角色(如
ROLE_ADMIN、ROLE_USER); - 给每个角色绑定一组资源访问权限(如
ROLE_ADMIN可访问所有资源,ROLE_USER仅可访问个人资源); - 再将角色分配给用户,用户通过所属角色获得对应的资源访问权限。
这种模式的优势在于减少权限分配的复杂度—— 当系统中有大量用户时,只需给用户分配角色,而非逐一分配权限,大幅降低维护成本。
层级结构:用户→角色→资源的三层映射
RBAC 模式在实际项目中的层级结构清晰,通常分为三层,对应数据库表设计与程序逻辑:
| 层级 | 核心作用 | 数据库表 / 程序组件 | 示例 |
|---|---|---|---|
| 用户层 | 系统访问主体 | sys_user 表 / UserDetails 接口 | 用户 admin、alice |
| 角色层 | 权限的集合载体,关联用户与资源 | sys_role 表 / GrantedAuthority | 角色 ROLE_ADMIN、ROLE_USER |
| 资源层 | 系统中需控制访问的对象(URL / 方法) | 权限配置(Web 层 / 方法级注解) | URL /admin/**、方法 deleteUser() |
映射关系:
- 用户与角色:多对多(一个用户可拥有多个角色,一个角色可分配给多个用户),通过
sys_user_role关联表实现; - 角色与资源:多对多(一个角色可访问多个资源,一个资源可允许多个角色访问),通过配置文件或数据库关联表实现。
从数据库设计到权限生效
以 “用户管理系统” 为例,完整实现 RBAC 模式的权限控制,包含数据库设计、用户详情加载、Web 层与方法级授权配置。
- 数据库表设计(RBAC 基础三表 + 关联表)
表名 核心字段 示例数据 sys_user(用户表)id、username、password1, admin, 123456;2, alice, 123456sys_role(角色表)id、role_name1, ROLE_ADMIN;2, ROLE_USERsys_user_role(用户 - 角色关联表)user_id、role_id1,1(admin 关联 ADMIN 角色);2,2(alice 关联 USER 角色) - 加载用户角色(自定义 UserDetailsService)
通过UserDetailsService接口,从数据库加载用户信息时,同步加载用户所属角色,并封装为GrantedAuthority集合:@Service public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate UserRoleRepository userRoleRepository;@Autowiredprivate RoleRepository roleRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 查询用户基本信息User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));// 2. 查询用户所属角色List<Long> roleIds = userRoleRepository.findRoleIdsByUserId(user.getId());List<Role> roles = roleRepository.findByIdIn(roleIds);// 3. 封装角色为 GrantedAuthority 集合(角色名需带 ROLE_ 前缀)List<GrantedAuthority> authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList());// 4. 返回自定义 UserDetails(包含用户信息与角色集合)return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),authorities);} } - Web 层授权配置(URL 级角色控制)
在SecurityFilterChain中配置 URL 与角色的对应关系,控制不同角色访问不同路径:@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth// 1. 管理员角色(ROLE_ADMIN)可访问 /admin/** 路径.requestMatchers("/admin/**").hasRole("ADMIN")// 2. USER 或 ADMIN 角色可访问 /user/** 路径.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")// 3. 公开路径(登录页、静态资源)允许所有用户访问.requestMatchers("/login", "/css/**", "/js/**").permitAll()// 4. 其他所有路径需登录(已认证用户).anyRequest().authenticated()).formLogin(form -> form.permitAll()) // 启用默认表单登录.logout(logout -> logout.permitAll()); // 启用默认退出登录return http.build(); }hasRole("ADMIN"):仅允许拥有ROLE_ADMIN角色的用户访问;hasAnyRole("USER", "ADMIN"):允许拥有ROLE_USER或ROLE_ADMIN角色的用户访问。
- 方法级授权配置(业务方法级角色控制)
通过@PreAuthorize注解在 Service 层方法上添加角色校验,实现更细粒度的权限控制:@Service public class UserService {// 1. 仅 ADMIN 角色可调用(删除用户)@PreAuthorize("hasRole('ADMIN')")public void deleteUser(Long userId) {userRepository.deleteById(userId);}// 2. USER 或 ADMIN 角色可调用(查询用户列表)@PreAuthorize("hasAnyRole('USER', 'ADMIN')")public List<UserDTO> findUserList() {return userRepository.findAll().stream().map(this::convertToDTO).collect(Collectors.toList());}// 3. 仅当前用户或 ADMIN 角色可调用(查询个人信息)@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")public UserDTO findUserByUsername(String username) {User user = userRepository.findByUsername(username).orElseThrow(() -> new RuntimeException("用户不存在"));return convertToDTO(user);} }
适用场景与局限性
- 适用场景
RBAC 模式因其 “简单、易维护” 的特点,适合以下场景:- 中小型系统:用户量不大、角色类型少(如仅管理员、普通用户两类角色);
- 权限逻辑稳定:角色与资源的关联关系长期不变(如管理员始终拥有所有权限,普通用户始终仅能访问个人资源);
- 粗粒度权限控制:仅需按角色控制 URL 或业务方法的访问(无需精确到 “某用户可操作某条数据”)。
- 局限性
当系统规模扩大或权限需求复杂时,RBAC 模式会暴露明显不足:- 权限过度授予:若一个角色包含多个权限,给用户分配该角色时,会自动获得所有权限,无法实现 “仅授予部分权限”(如普通用户需 “查看订单” 但无需 “导出订单”,但角色可能同时包含这两个权限);
- 角色爆炸:当需要精细化控制时,需创建大量角色(如
ROLE_USER_VIEW、ROLE_USER_EDIT、ROLE_USER_DELETE),导致角色数量激增,维护成本升高; - 无法满足数据级权限:无法控制 “用户只能访问自己创建的数据”(如普通用户仅能查看自己的订单,而非所有订单),需额外结合方法级校验(如
@PreAuthorize中的数据归属判断)。
RBAC 模式的常见扩展
为弥补基础 RBAC 的局限性,实际项目中常对其进行简单扩展,平衡 “易用性” 与 “灵活性”:
- 角色分层(基础角色 + 附加角色)
- 设计思路:将角色分为 “基础角色”(如
ROLE_USER、ROLE_ADMIN)和 “附加角色”(如ROLE_ORDER_EXPORT、ROLE_DATA_VIEW); - 优势:基础角色保证核心权限,附加角色实现精细化权限补充,避免单一角色权限过度。
- 设计思路:将角色分为 “基础角色”(如
- 结合方法级数据校验
- 针对 “数据级权限” 需求,在 RBAC 基础上,通过
@PreAuthorize表达式添加数据归属校验:// 普通用户仅能修改自己的信息,管理员可修改所有用户 @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')") public void updateUser(Long userId, UserDTO dto) {// 业务逻辑 }
- 针对 “数据级权限” 需求,在 RBAC 基础上,通过
基于权限(PBAC)的访问控制
基于权限的访问控制(Permission-Based Access Control,简称 PBAC)是比 RBAC 更细粒度的权限设计模式。它跳过 “角色” 这一中间层,直接将 “权限” 与用户 / 资源绑定,核心是 “按操作许可控制访问”,而非 “按身份归类控制访问”。这种模式更适合中大型系统、操作类型多或权限需灵活调整的场景。
核心原理:权限作为 “操作许可” 的直接载体
PBAC 的核心思想是用 “权限” 描述 “用户可执行的具体操作”,直接建立 “用户 - 权限 - 资源” 的映射关系:
- 定义细粒度的权限标识(如
user:read表示 “查看用户”、user:delete表示 “删除用户”),每个权限对应一个具体操作; - 给用户分配所需的权限(而非角色),用户拥有的权限集合决定了其可执行的操作;
- 配置资源(URL / 方法)与权限的关联关系,只有拥有对应权限的用户才能访问资源。
这种模式的优势在于权限控制更精准—— 可按需给用户分配单个操作权限,避免 RBAC 中 “角色包含冗余权限” 的问题,严格遵循 “最小权限原则”。
层级结构:用户→权限→资源的三层映射
PBAC 模式的层级结构比 RBAC 更直接,去掉了 “角色” 中间层,聚焦 “操作许可” 与 “资源” 的绑定:
| 层级 | 核心作用 | 数据库表 / 程序组件 | 示例 |
|---|---|---|---|
| 用户层 | 系统访问主体 | sys_user 表 / UserDetails 接口 | 用户 admin、alice |
| 权限层 | 具体操作的许可标识 | sys_permission 表 / GrantedAuthority | 权限 user:read、user:delete |
| 资源层 | 系统中需控制访问的对象(URL / 方法) | 权限配置(Web 层 / 方法级注解) | URL /api/users(GET)、方法 deleteUser() |
映射关系:
- 用户与权限:多对多(一个用户可拥有多个权限,一个权限可分配给多个用户),通过
sys_user_permission关联表实现; - 权限与资源:多对多(一个权限可关联多个资源,一个资源可关联多个权限),通过配置文件或数据库表实现。
从数据库设计到权限生效
以 “用户管理系统” 为例,完整实现 PBAC 模式的权限控制,包含数据库设计、用户权限加载、Web 层与方法级授权配置。
- 数据库表设计(PBAC 核心三表 + 关联表)
表名 核心字段 示例数据 sys_user(用户表)id、username、password1, admin, 123456;2, alice, 123456sys_permission(权限表)id、perm_code、description1, user:read, 查看用户;2, user:delete, 删除用户sys_user_permission(用户 - 权限关联表)user_id、perm_id1,1(admin 有 user:read);1,2(admin 有 user:delete);2,1(alice 有 user:read) - 加载用户权限(自定义 UserDetailsService)
从数据库加载用户信息时,同步加载用户拥有的权限,封装为GrantedAuthority集合(权限标识直接作为getAuthority()返回值):@Service public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate UserPermissionRepository userPermissionRepository;@Autowiredprivate PermissionRepository permissionRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 查询用户基本信息User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));// 2. 查询用户拥有的权限List<Long> permIds = userPermissionRepository.findPermIdsByUserId(user.getId());List<Permission> permissions = permissionRepository.findByIdIn(permIds);// 3. 封装权限为 GrantedAuthority 集合(直接使用 perm_code 作为权限字符串)List<GrantedAuthority> authorities = permissions.stream().map(perm -> new SimpleGrantedAuthority(perm.getPermCode())).collect(Collectors.toList());// 4. 返回 UserDetails(包含用户信息与权限集合)return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),authorities);} } - Web 层授权配置(URL 级权限控制)
在SecurityFilterChain中配置 URL 与权限的对应关系,控制不同权限的用户访问不同路径(尤其适合 REST API 按 HTTP 方法区分权限):
按 “HTTP 方法 + URL” 组合配置权限,精准匹配 REST API 的 CRUD 操作(如@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth// 1. GET 请求 /api/users 需 user:read 权限.requestMatchers(HttpMethod.GET, "/api/users").hasAuthority("user:read")// 2. DELETE 请求 /api/users/** 需 user:delete 权限.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasAuthority("user:delete")// 3. POST 请求 /api/users 需 user:create 权限.requestMatchers(HttpMethod.POST, "/api/users").hasAuthority("user:create")// 4. PUT 请求 /api/users/** 需 user:update 权限.requestMatchers(HttpMethod.PUT, "/api/users/**").hasAuthority("user:update")// 5. 公开路径放行.requestMatchers("/login", "/swagger-ui/**").permitAll()// 6. 其他路径需登录.anyRequest().authenticated()).formLogin(form -> form.permitAll()).logout(logout -> logout.permitAll());return http.build(); }GET /api/users对应 “查看” 权限,DELETE /api/users/**对应 “删除” 权限),符合前后端分离项目的 API 设计习惯。 - 方法级授权配置(业务方法级权限控制)
通过@PreAuthorize注解在 Service 层方法上添加权限校验,实现业务逻辑与权限的深度绑定:@Service public class UserService {// 1. 查看用户需 user:read 权限@PreAuthorize("hasAuthority('user:read')")public UserDTO findUserById(Long id) {User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("用户不存在"));return convertToDTO(user);}// 2. 删除用户需 user:delete 权限@PreAuthorize("hasAuthority('user:delete')")public void deleteUser(Long id) {userRepository.deleteById(id);}// 3. 批量操作需同时具备 user:read 和 user:delete 权限@PreAuthorize("hasAuthority('user:read') and hasAuthority('user:delete')")public void batchDeleteUsers(List<Long> ids) {userRepository.deleteAllById(ids);} }
PBAC 与 RBAC 的核心对比
| 对比维度 | 基于角色(RBAC) | 基于权限(PBAC) |
|---|---|---|
| 控制粒度 | 较粗(按角色归类,一个角色包含多个权限) | 极细(按单个操作许可,精准控制) |
| 核心载体 | 角色(如 ROLE_ADMIN) | 权限(如 user:delete) |
| 权限分配方式 | 用户→角色(间接获得角色关联的所有权限) | 用户→权限(直接获得所需的单个权限) |
| 适用场景 | 中小型系统、角色少、权限稳定 | 中大型系统、操作类型多、权限需灵活调整 |
| 维护成本 | 低(角色数量少,分配简单) | 高(权限数量多,需精准分配) |
| 遵循原则 | 便捷优先,可能存在权限冗余 | 最小权限原则,无冗余权限 |
| 典型应用 | 后台管理系统(仅管理员、普通用户两类角色) | 企业级系统(如 ERP、CRM,多岗位多操作权限) |
PBAC 的扩展与最佳实践
- 权限命名规范
为避免权限标识混乱,建议统一采用「资源:操作」的格式命名(如user:read、order:create),其中:- 资源:对应业务实体(如
user、order、document); - 操作:对应具体动作(如
read、create、update、delete、export)。
- 资源:对应业务实体(如
- 结合角色简化权限分配(PBAC+RBAC 混合模式)
在中大型系统中,纯 PBAC 会因权限数量多导致分配繁琐,可引入 “角色” 作为 “权限集合” 的载体,形成混合模式:- 定义 “角色” 关联一组权限(如
ROLE_USER关联user:read、user:update;ROLE_ADMIN关联所有权限); - 给用户分配角色(快速获得基础权限),同时支持给用户单独追加特殊权限(如给某用户额外分配
user:export); - 授权校验时,同时校验角色和权限(如
@PreAuthorize("hasRole('USER') and hasAuthority('user:export')"))。
- 定义 “角色” 关联一组权限(如
- 权限的动态管理
对于权限频繁变更的系统,建议将 “权限 - 资源” 的关联关系存储在数据库中(如sys_resource_permission表),启动时从数据库加载配置,避免硬编码在代码中:// 从数据库加载 URL-权限映射,动态配置 Web 层授权 @Bean public SecurityFilterChain filterChain(HttpSecurity http, PermissionRepository permRepo) throws Exception {// 1. 从数据库查询所有权限-资源映射List<PermissionResource> permResources = permRepo.findAllPermissionResources();// 2. 构建动态授权规则var authManagerBuilder = RequestMatcherDelegatingAuthorizationManager.builder();for (PermissionResource pr : permResources) {// 构建 URL 匹配器(支持 HTTP 方法)RequestMatcher matcher = new AntPathRequestMatcher(pr.getUrl(), pr.getHttpMethod());// 绑定权限(如 "/api/users" GET 方法绑定 "user:read")authManagerBuilder.add(matcher, AuthorityAuthorizationManager.hasAuthority(pr.getPermCode()));}// 3. 配置动态授权http.authorizeHttpRequests(auth -> auth.anyRequest().access(authManagerBuilder.build()));return http.build(); }
动态权限设计
Spring Security 默认的权限配置是「静态硬编码」(如 .requestMatchers("/admin/**").hasRole("ADMIN")),但实际项目中,权限规则(哪些 URL 需要哪些权限)常需存储在数据库中,支持灵活修改(如管理员在后台配置权限)。动态权限设计的核心是「从数据库加载 URL 与权限的映射关系」,替代硬编码配置,实现权限的可配置化与可维护性。
核心目标:解决静态配置的局限性
静态权限配置的问题在于「修改权限需改代码、重启服务」,无法满足生产环境中灵活调整权限的需求。动态权限设计需实现以下目标:
- 权限规则存储在数据库,支持增删改查(无需改代码);
- 系统启动时自动从数据库加载权限规则,生效为 Spring Security 授权配置;
- 后续可扩展「运行时动态刷新权限」(无需重启服务)。
数据库设计:存储 URL - 权限映射关系
动态权限的核心是设计「资源 - 权限」关联表,记录每个 URL(资源)对应的权限要求。结合前面的 RBAC/PBAC 模型,完整表结构如下(新增资源与权限关联表):
| 表名 | 核心字段 | 作用 | 示例数据 |
|---|---|---|---|
sys_resource(资源表) | id、url、http_method、name | 存储系统中的资源(URL)信息 | 1, /api/users, GET, 用户列表查询;2, /api/users/**, DELETE, 用户删除 |
sys_permission(权限表) | id、perm_code、description | 存储权限标识(如 PBAC 中的操作许可) | 1, user:read, 查看用户;2, user:delete, 删除用户 |
sys_resource_permission(资源 - 权限关联表) | resource_id、permission_id | 建立 URL 与权限的多对多关系 | 1,1(/api/users GET 需 user:read);2,2(/api/users/** DELETE 需 user:delete) |
设计说明:
sys_resource.url:支持 Ant 风格通配符(如/api/users/**),与 Spring Security 的AntPathRequestMatcher兼容;sys_resource.http_method:指定 HTTP 方法(如 GET、POST、DELETE),实现同一 URL 不同方法的权限区分;- 多对多关联:一个 URL 可对应多个权限(如
/api/usersPOST 需user:create和role:admin),一个权限可对应多个 URL(如user:read对应/api/usersGET 和/api/user/{id}GET)。
从数据库加载权限规则
实现动态权限的关键步骤是「查询数据库中的资源 - 权限映射,转换为 Spring Security 可识别的授权规则」,核心通过 SecurityMetadataService 服务封装加载逻辑。
- 定义实体类与数据访问层
实体类Resource.java、Permission.java、ResourcePermission.java
数据访问层 Repository// 资源实体(URL) @Data @Entity @Table(name = "sys_resource") public class Resource {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String url; // 资源 URL(支持 Ant 通配符)private String httpMethod; // HTTP 方法(GET/POST/DELETE,null 表示任意方法)private String name; // 资源名称(如“用户列表查询”) }// 权限实体 @Data @Entity @Table(name = "sys_permission") public class Permission {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String permCode; // 权限标识(如 user:read)private String description; // 权限描述 }// 资源-权限关联实体 @Data @Entity @Table(name = "sys_resource_permission") public class ResourcePermission {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@ManyToOne@JoinColumn(name = "resource_id")private Resource resource;@ManyToOne@JoinColumn(name = "permission_id")private Permission permission; }// 资源 Repository public interface ResourceRepository extends JpaRepository<Resource, Long> { }// 权限 Repository public interface PermissionRepository extends JpaRepository<Permission, Long> { }// 资源-权限关联 Repository(查询资源对应的所有权限) public interface ResourcePermissionRepository extends JpaRepository<ResourcePermission, Long> {List<ResourcePermission> findByResourceId(Long resourceId); } - 封装权限加载服务
SecurityMetadataService
该服务的核心作用是「查询数据库中的资源 - 权限映射,转换为 Spring Security 授权规则所需的RequestMatcher(URL 匹配器)和AuthorizationManager(权限管理器)」:@Service public class SecurityMetadataService {@Autowiredprivate ResourceRepository resourceRepo;@Autowiredprivate ResourcePermissionRepository resourcePermissionRepo;/*** 从数据库加载所有资源-权限映射,转换为 Spring Security 授权规则* @return key:URL 匹配器(RequestMatcher);value:该 URL 对应的权限管理器(AuthorizationManager)*/public Map<RequestMatcher, AuthorizationManager<HttpServletRequest>> loadResourcePermissions() {Map<RequestMatcher, AuthorizationManager<HttpServletRequest>> resourceAuthMap = new LinkedHashMap<>();// 1. 查询所有资源(URL)List<Resource> resources = resourceRepo.findAll();for (Resource resource : resources) {// 2. 查询当前资源对应的所有权限List<ResourcePermission> resourcePermissions = resourcePermissionRepo.findByResourceId(resource.getId());List<String> permCodes = resourcePermissions.stream().map(rp -> rp.getPermission().getPermCode()).collect(Collectors.toList());// 3. 构建 URL 匹配器(支持 Ant 风格和 HTTP 方法)String httpMethod = resource.getHttpMethod();RequestMatcher requestMatcher = new AntPathRequestMatcher(resource.getUrl(), httpMethod != null ? httpMethod : "GET" // 无方法指定时默认匹配 GET);// 4. 构建权限管理器(需满足所有权限或任意一个权限,此处以“任意一个”为例)AuthorizationManager<HttpServletRequest> authManager;if (permCodes.isEmpty()) {// 无权限要求:允许所有已认证用户访问authManager = AuthenticatedAuthorizationManager.authenticated();} else {// 有权限要求:满足任意一个权限即可访问(hasAnyAuthority)authManager = AuthorityAuthorizationManager.hasAnyAuthority(permCodes.toArray(new String[0]));}// 5. 存入映射表(LinkedHashMap 保证顺序,匹配时按存储顺序生效)resourceAuthMap.put(requestMatcher, authManager);}return resourceAuthMap;} }AntPathRequestMatcher:支持 Ant 风格 URL(如/api/users/**)和指定 HTTP 方法,与 Spring Security 静态配置的 URL 匹配逻辑一致;AuthorityAuthorizationManager.hasAnyAuthority:表示 “用户拥有任意一个指定权限即可访问”,若需 “同时拥有所有权限”,可改用AuthorityAuthorizationManager.hasAllAuthorities;LinkedHashMap:保证资源加载顺序,匹配时按 “先精确后模糊” 的顺序生效(需在数据库中按优先级排序存储)。
将动态规则注册到 SecurityFilterChain
通过 SecurityMetadataService 加载数据库中的权限规则后,需将其注册到 SecurityFilterChain 中,替代静态的 .authorizeHttpRequests() 配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Autowiredprivate SecurityMetadataService securityMetadataService;@Autowiredprivate CustomUserDetailsService userDetailsService;// 密码编码器(测试用,生产环境用 BCryptPasswordEncoder)@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}// 认证提供者(加载用户权限)@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userDetailsService);provider.setPasswordEncoder(passwordEncoder());return provider;}// 核心:配置动态权限的 SecurityFilterChain@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {// 1. 从数据库加载资源-权限映射Map<RequestMatcher, AuthorizationManager<HttpServletRequest>> resourceAuthMap = securityMetadataService.loadResourcePermissions();// 2. 构建动态授权管理器(RequestMatcherDelegatingAuthorizationManager)RequestMatcherDelegatingAuthorizationManager.Builder authManagerBuilder = RequestMatcherDelegatingAuthorizationManager.builder();// 将数据库加载的规则逐一添加到授权管理器resourceAuthMap.forEach(authManagerBuilder::add);// 兜底规则:未匹配到任何资源的请求,需已认证(可根据业务调整)authManagerBuilder.anyRequest(AuthenticatedAuthorizationManager.authenticated());RequestMatcherDelegatingAuthorizationManager authManager = authManagerBuilder.build();// 3. 配置 HTTP 安全规则,使用动态授权管理器http.authorizeHttpRequests(auth -> auth.anyRequest().access(authManager) // 所有请求都使用动态授权管理器).formLogin(form -> form.permitAll()).logout(logout -> logout.permitAll()).csrf(csrf -> csrf.disable()); // 前后端分离测试用,生产环境需开启// 4. 注册认证提供者http.authenticationProvider(authenticationProvider());return http.build();}
}
RequestMatcherDelegatingAuthorizationManager 是 Spring Security 6.x+ 提供的 “动态授权管理器”,支持通过 add() 方法动态添加 RequestMatcher 与 AuthorizationManager 的映射,完全替代静态配置。
核心原理:动态授权的执行流程
动态权限的执行流程与静态配置一致,核心差异在于 “授权规则的来源”(数据库 vs 硬编码),流程如下:

关键节点:
- 步骤 5:
RequestMatcherDelegatingAuthorizationManager按LinkedHashMap的存储顺序匹配 URL,确保 “精确 URL 优先于模糊 URL”(需在数据库中按优先级排序); - 步骤 7:
AuthorizationManager的校验逻辑与静态配置一致(如hasAnyAuthority检查用户是否拥有指定权限)。
注意事项与优化建议
- URL 匹配顺序问题
- 数据库中存储的资源(URL)需按 “精确到模糊” 的顺序排序(如
/api/users/1优先于/api/users/**),否则模糊 URL 会优先匹配,导致精确 URL 的规则失效; - 优化方案:在
sys_resource表中添加sort字段,查询时按sort升序排列,确保匹配顺序正确。
- 数据库中存储的资源(URL)需按 “精确到模糊” 的顺序排序(如
- 权限加载时机问题
- 目前的实现是 “系统启动时加载一次权限”,若数据库中的权限规则修改,需重启服务才能生效;
- 后续优化:结合 “动态刷新权限”(第六模块内容),实现 “权限修改后无需重启,实时生效”。
- 性能优化
- 系统启动时加载权限规则仅执行一次,性能影响可忽略;
- 若资源数量极大(如 thousands 级别),可在
SecurityMetadataService中添加缓存(如 Caffeine、Redis),避免频繁查询数据库(但启动时加载通常无需缓存)。
动态刷新权限表
在动态权限设计中,仅实现 “启动时从数据库加载权限” 还不够 —— 实际业务中,管理员可能随时在后台修改权限规则(如新增 URL 权限、调整权限关联),此时需要系统在运行时动态刷新权限配置,无需重启服务。
核心问题:为何需要动态刷新?
启动时加载权限的方案存在明显局限:
- 若数据库中权限规则(如
sys_resource_permission表的关联关系)发生变化,必须重启服务才能生效; - 对于生产环境的核心系统,频繁重启会导致服务中断,影响可用性。
动态刷新的目标是:当权限规则变更时,系统能自动(或手动触发)重新加载权限配置,新规则实时生效。
刷新原理:替换授权管理器实例
Spring Security 的授权逻辑由 AuthorizationManager 驱动,动态刷新的核心是:当权限规则变更时,重新构建 AuthorizationManager 实例,并替换当前正在使用的旧实例。
关键组件关系如下:
AuthorizationFilter是拦截请求并执行授权的过滤器,它依赖AuthorizationManager进行权限判断;- 若能在运行时用新的
AuthorizationManager(基于最新权限规则构建)替换旧实例,就能实现权限的动态生效。
实现方案一:定时任务刷新(适合权限变更不频繁场景)
通过定时任务(如每 1 分钟)定期查询数据库,重新构建 AuthorizationManager,适合权限变更频率低、对实时性要求不高的场景。
- 用线程安全的容器管理授权管理器
使用AtomicReference存储AuthorizationManager实例,确保多线程环境下的可见性和原子性:@Service public class DynamicAuthorizationManager {// 原子引用存储当前生效的授权管理器(线程安全)private final AtomicReference<RequestMatcherDelegatingAuthorizationManager> currentAuthManager = new AtomicReference<>();@Autowiredprivate SecurityMetadataService securityMetadataService;// 初始化:系统启动时加载首次权限@PostConstructpublic void init() {refresh();}// 刷新权限:重新构建授权管理器public void refresh() {// 1. 从数据库加载最新的资源-权限映射Map<RequestMatcher, AuthorizationManager<HttpServletRequest>> resourceAuthMap = securityMetadataService.loadResourcePermissions();// 2. 构建新的授权管理器RequestMatcherDelegatingAuthorizationManager newAuthManager = RequestMatcherDelegatingAuthorizationManager.builder().apply((builder) -> resourceAuthMap.forEach(builder::add)) // 添加所有资源-权限规则.anyRequest(AuthenticatedAuthorizationManager.authenticated()) // 兜底规则.build();// 3. 原子替换当前授权管理器currentAuthManager.set(newAuthManager);}// 获取当前生效的授权管理器public RequestMatcherDelegatingAuthorizationManager getCurrentAuthManager() {return currentAuthManager.get();} } - 配置定时任务触发刷新
使用 Spring 的@Scheduled注解,定时执行refresh()方法:@Configuration @EnableScheduling // 启用定时任务 public class ScheduledConfig {@Autowiredprivate DynamicAuthorizationManager dynamicAuthManager;// 每 60 秒刷新一次权限(可根据业务调整频率)@Scheduled(fixedDelay = 60000)public void schedulePermissionRefresh() {dynamicAuthManager.refresh();log.info("定时刷新权限规则完成");} } - SecurityFilterChain 引用动态授权管理器
修改安全配置,让AuthorizationFilter使用DynamicAuthorizationManager中动态更新的实例:@Bean public SecurityFilterChain filterChain(HttpSecurity http, DynamicAuthorizationManager dynamicAuthManager) throws Exception {http.authorizeHttpRequests(auth -> auth// 所有请求都使用当前生效的授权管理器.anyRequest().access((authentication, request) -> dynamicAuthManager.getCurrentAuthManager().check(authentication, request)))// 其他配置(登录、退出等).formLogin(form -> form.permitAll()).logout(logout -> logout.permitAll()).csrf(csrf -> csrf.disable());return http.build(); }
实现方案二:事件驱动刷新(适合权限实时变更场景)
当权限规则在数据库中发生变更时(如管理员在后台点击 “保存” 按钮),通过 “事件发布 - 订阅” 机制立即触发刷新,适合对实时性要求高的场景。
- 定义权限变更事件
// 权限变更事件:当权限规则修改时发布 public class PermissionChangedEvent extends ApplicationEvent {public PermissionChangedEvent(Object source) {super(source);} } - 发布事件(在权限修改接口中触发)
在修改权限的 Service 或 Controller 中,当权限规则保存成功后,发布PermissionChangedEvent:@Service public class PermissionService {@Autowiredprivate ApplicationEventPublisher eventPublisher;@Autowiredprivate ResourcePermissionRepository resourcePermissionRepo;// 修改资源-权限关联关系@Transactionalpublic void updateResourcePermissions(Long resourceId, List<Long> permIds) {// 1. 删除旧关联resourcePermissionRepo.deleteByResourceId(resourceId);// 2. 保存新关联(省略具体逻辑)// ...// 3. 发布权限变更事件eventPublisher.publishEvent(new PermissionChangedEvent(this));} } - 订阅事件并触发刷新
让DynamicAuthorizationManager实现ApplicationListener接口,监听事件并刷新权限:
事件发布机制可结合消息队列(如 RabbitMQ、Kafka)实现分布式系统的权限同步 —— 多实例部署时,一个实例修改权限后,通过消息队列通知所有实例刷新权限。@Service public class DynamicAuthorizationManager implements ApplicationListener<PermissionChangedEvent> {// 复用方案一中的 currentAuthManager、init()、refresh() 方法...// 监听权限变更事件,立即刷新@Overridepublic void onApplicationEvent(PermissionChangedEvent event) {refresh();log.info("接收到权限变更事件,已刷新权限规则");} }
线程安全与性能优化
动态刷新涉及多线程操作(如定时任务线程、请求处理线程),需重点关注线程安全和性能问题:
- 线程安全保障
- 使用
AtomicReference存储AuthorizationManager实例,确保替换操作的原子性; RequestMatcherDelegatingAuthorizationManager本身是线程安全的(无状态设计),可在多线程中共享使用。
- 使用
- 性能优化
- 刷新时避免阻塞请求:
refresh()方法构建新授权管理器的过程(查询数据库、构建规则)可能耗时,需异步执行:@Async // 异步执行刷新,不阻塞事件发布线程 public void refresh() {// 构建新授权管理器的逻辑... } - 缓存数据库查询结果:若
loadResourcePermissions()方法查询数据库耗时,可添加本地缓存(如 Caffeine),减少数据库压力:
注意:缓存需在刷新前手动清除(@CacheEvict),确保加载最新数据。// 在 SecurityMetadataService 中添加缓存 @Cacheable(value = "resourcePermissions", key = "'all'") public Map<RequestMatcher, AuthorizationManager<HttpServletRequest>> loadResourcePermissions() {// 数据库查询逻辑... }
- 刷新时避免阻塞请求:
