授权(Authorization)

基于角色与基于权限的设计模式

基本概念

本质定义:宏观身份 vs 微观操作

角色和权限是 Spring Security 权限控制的两个核心概念,但定位截然不同:

  1. 角色(Role):用户的宏观身份标识
    • 核心定位:对用户群体的「身份归类」,是一组权限的集合,描述用户在系统中的整体角色(如管理员、普通用户)。
    • 典型场景:系统中需要区分不同身份的用户(如管理员可操作所有功能,普通用户仅能操作个人数据)。
    • 示例ROLE_ADMIN(管理员角色)、ROLE_USER(普通用户角色)、ROLE_MANAGER(经理角色)。
  2. 权限(Authority):用户的微观操作许可
    • 核心定位:对具体业务操作的「细粒度许可」,描述用户可执行的具体动作(如查看用户、删除订单)。
    • 典型场景:系统需要精准控制用户的操作范围(如普通用户仅能「查看」订单,不能「删除」订单)。
    • 示例user:read(查看用户)、user:delete(删除用户)、order:create(创建订单)。

技术底层:统一实现,差异仅在约定

在 Spring Security 内部,角色和权限本质上没有区别,均通过 GrantedAuthority 接口实现,核心差异仅在于「是否遵循 ROLE_ 前缀约定」。

  1. 核心接口:GrantedAuthority
    Spring Security 中所有权限相关的信息都封装为 GrantedAuthority 实例,该接口仅有一个方法:
    public interface GrantedAuthority {
        // 返回权限字符串(角色或权限的核心标识)
        String getAuthority();
    }
    
    角色和权限的实例化均使用其实现类 SimpleGrantedAuthority
    // 角色实例(遵循 ROLE_ 前缀约定)
    GrantedAuthority adminRole = new SimpleGrantedAuthority("ROLE_ADMIN");
    // 权限实例(无固定前缀,通常为「资源:操作」格式)
    GrantedAuthority userReadPerm = new SimpleGrantedAuthority("user:read");
    
  2. 用户与权限的关联: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_ 前缀)+ 权限(自定义格式)统一封装为 GrantedAuthority
            this.authorities = new ArrayList<>();
            // 添加角色
            this.authorities.add(new SimpleGrantedAuthority(user.getRole()));
            // 添加权限
            permCodes.forEach(perm -> authorities.add(new SimpleGrantedAuthority(perm)));
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
    }
    
  3. 授权校验的本质:字符串匹配
    Spring Security 的授权校验核心是「权限集合中是否包含目标字符串」,角色和权限的校验差异仅在于目标字符串的格式:
    校验方法 目标字符串格式 底层匹配逻辑
    hasRole("ADMIN") 自动拼接 ROLE_ 前缀 → ROLE_ADMIN 检查用户权限集合中是否包含 ROLE_ADMIN
    hasAuthority("user:read") 直接使用传入字符串 → user:read 检查用户权限集合中是否包含 user:read
    hasAnyRole("ADMIN", "USER") 拼接为 ROLE_ADMINROLE_USER 检查用户权限集合中是否包含任意一个角色字符串
    hasAnyAuthority("user:read", "user:write") 直接使用传入字符串 检查用户权限集合中是否包含任意一个权限字符串
    关键结论:hasRole("ADMIN") 等价于 hasAuthority("ROLE_ADMIN"),前者是后者的语法糖,简化了角色校验的写法。

数据库设计:从概念到存储的映射

在实际项目中,角色和权限的存储需通过数据库表结构体现,核心遵循「用户 - 角色 - 权限」的关联关系(RBAC 基础模型)。

  1. 核心表结构设计
    表名 核心字段 作用
    sys_user(用户表) idusernamepassword 存储用户基础信息
    sys_role(角色表) idrole_name 存储角色信息(角色名需遵循 ROLE_ 前缀)
    sys_permission(权限表) idperm_codedescription 存储权限信息(权限码通常为「资源:操作」格式)
    sys_user_role(用户 - 角色关联表) user_idrole_id 建立用户与角色的多对多关系
    sys_role_permission(角色 - 权限关联表) role_idperm_id 建立角色与权限的多对多关系
  2. 表数据示例
    • 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
  3. 数据查询逻辑
    当用户登录时,系统通过以下逻辑加载其角色和权限:
    1. 根据用户名查询 sys_user 得到用户信息;
    2. 通过 sys_user_role 关联查询用户拥有的角色;
    3. 通过 sys_role_permission 关联查询每个角色对应的权限;
    4. 将角色(role_name)和权限(perm_code)封装为 GrantedAuthority 集合,存入 UserDetails

核心差异对比

为了更清晰地梳理两者的区别,整理如下对比表:

维度 角色(Role) 权限(Authority)
核心定位 宏观身份标识(用户归类) 微观操作许可(具体动作)
命名约定 必须以 ROLE_ 为前缀(否则 hasRole 无法识别) 无固定前缀,推荐「资源:操作」格式(如 user:read
粒度级别 较粗(一组权限的集合) 极细(单个操作的许可)
适用场景 简单系统的身份区分(如管理员 vs 普通用户) 复杂系统的细粒度操作控制(如查看 / 删除 / 编辑)
底层实现 SimpleGrantedAuthority(字符串含 ROLE_ 前缀) SimpleGrantedAuthority(字符串自定义格式)
校验方法 hasRole()hasAnyRole() hasAuthority()hasAnyAuthority()

注意事项

  1. 角色前缀遗漏导致校验失败
    • 问题:配置 hasRole("ADMIN") 后,用户虽拥有 ADMIN 权限字符串,但校验仍失败;
    • 原因hasRole 会自动拼接 ROLE_ 前缀,实际校验的是 ROLE_ADMIN,而用户权限字符串为 ADMIN(无前缀);
    • 解决方案
      1. 角色存储时添加 ROLE_ 前缀(如 ROLE_ADMIN);
      2. 若不想使用前缀,改用 hasAuthority("ADMIN") 直接校验权限字符串。
  2. 权限命名不规范导致维护困难
    • 问题:权限字符串格式混乱(如 readUserdelete_userORDERCREATE),后期难以维护;
    • 解决方案:统一权限命名规范,推荐「资源:操作」格式(如 user:readorder:delete),清晰区分资源和操作类型。
  3. 混淆角色与权限的适用场景
    • 问题:在复杂系统中仅使用角色控制权限,导致权限过度授予(如普通用户需「编辑自己的资料」,却被分配了包含所有用户编辑权限的角色);
    • 解决方案:复杂系统优先使用权限进行细粒度控制,角色仅作为权限的集合载体,避免直接通过角色控制具体操作。

理解角色与权限

核心桥梁:GrantedAuthority 接口的统一抽象

Spring Security 对角色和权限的理解,完全基于 GrantedAuthority 接口 ——它不区分 “角色” 和 “权限” 的语义,只认接口返回的字符串。所有与权限相关的判断,本质都是对 GrantedAuthority.getAuthority() 返回值的字符串匹配。

  1. 接口的核心作用
    GrantedAuthority 是 Spring Security 权限体系的 “最小单元”,其唯一职责是提供一个权限标识字符串。无论是角色(如 ROLE_ADMIN)还是权限(如 user:read),在 Spring Security 眼中都是 “一串带标识的字符串”,区别仅在于开发者赋予的语义和约定(如 ROLE_ 前缀)。
  2. 实例化与存储
    所有角色和权限都通过 SimpleGrantedAuthorityGrantedAuthority 的默认实现)实例化,最终存储在 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 的授权校验(如 hasRolehasAuthority),本质是 “检查用户的权限集合中,是否包含目标字符串”。不同校验方法的差异,仅在于 “目标字符串是否需要加工”。

  1. 角色校验:hasRolehasAnyRole 的逻辑
    hasRole 方法会自动为传入的角色名拼接 ROLE_ 前缀,再去用户的权限集合中匹配字符串:
    • 示例:hasRole("ADMIN") 的校验流程:
      1. 拼接前缀:"ADMIN""ROLE_ADMIN"
      2. 遍历用户的 GrantedAuthority 集合,检查是否有 getAuthority() 返回 "ROLE_ADMIN" 的实例;
      3. 若存在,校验通过;否则,校验失败。
    • hasAnyRole 逻辑类似,只是支持多个角色名,只要有一个拼接后的字符串匹配,即通过校验:
      // 校验逻辑:用户权限集合中是否包含 "ROLE_ADMIN" 或 "ROLE_USER"
      hasAnyRole("ADMIN", "USER") 
      
  2. 权限校验:hasAuthorityhasAnyAuthority 的逻辑
    hasAuthority 方法直接使用传入的字符串,不做任何加工,直接去用户的权限集合中匹配:
    • 示例:hasAuthority("user:read") 的校验流程:
      1. 直接使用目标字符串:"user:read"
      2. 遍历用户的 GrantedAuthority 集合,检查是否有 getAuthority() 返回 "user:read" 的实例;
      3. 若存在,校验通过;否则,校验失败。
    • hasAnyAuthority 支持多个权限字符串,只要有一个匹配,即通过校验:
      // 校验逻辑:用户权限集合中是否包含 "user:read" 或 "user:delete"
      hasAnyAuthority("user:read", "user:delete")
      
  3. 两种校验的等价关系
    通过底层逻辑可推导出:角色校验是权限校验的 “语法糖”,两者可互相转换:
    角色校验方法 等价的权限校验方法 本质匹配的字符串
    hasRole("ADMIN") hasAuthority("ROLE_ADMIN") "ROLE_ADMIN"
    hasAnyRole("A", "B") hasAnyAuthority("ROLE_A", "ROLE_B") "ROLE_A""ROLE_B"

从代码看 Spring Security 的理解逻辑

通过 Web 层授权和方法级授权的配置示例,可直观看到 Spring Security 对角色与权限的处理差异:

  1. 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()
    );
    
  2. 方法级授权配置
    @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 对角色与权限的 “无差别对待”,容易导致开发者因 “语义约定” 与 “代码实现” 不符而踩坑,需重点关注以下两点:

  1. ROLE_ 前缀是 “约定” 而非 “强制”
    • Spring Security 仅在 hasRole/hasAnyRole 方法中会自动拼接 ROLE_ 前缀,若角色字符串未加前缀,这些方法会校验失败;
    • 若数据库中存储的角色是 ADMIN(无前缀),使用 hasRole("ADMIN") 会匹配 ROLE_ADMIN,导致校验失败,此时有两种解决方案:
      1. 存储角色时添加 ROLE_ 前缀(推荐,符合约定);
      2. 改用 hasAuthority("ADMIN") 直接校验(不推荐,破坏角色语义)。
  2. 权限字符串格式需 “自定义约定”
    • Spring Security 对权限字符串的格式无强制要求(如 user:readREAD_USERuser.read 均可),但需在项目中统一格式,避免后期维护混乱;
    • 推荐格式:资源:操作(如 user:readorder:delete),清晰区分 “操作对象” 和 “操作类型”,便于扩展和理解。

hasRole 的实现逻辑

通过 Spring Security 源码(SecurityExpressionRoot 类)可进一步验证 hasRole 的前缀拼接逻辑:

public class SecurityExpressionRoot implements SecurityExpressionOperations {
    // 角色前缀,默认是 "ROLE_"
    private String defaultRolePrefix = "ROLE_";

    // hasRole 方法实现
    public boolean hasRole(String role) {
        // 拼接前缀后调用 hasAuthority
        return 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_ADMINROLE_USER);
  • 给每个角色绑定一组资源访问权限(如 ROLE_ADMIN 可访问所有资源,ROLE_USER 仅可访问个人资源);
  • 再将角色分配给用户,用户通过所属角色获得对应的资源访问权限。

这种模式的优势在于减少权限分配的复杂度—— 当系统中有大量用户时,只需给用户分配角色,而非逐一分配权限,大幅降低维护成本。

层级结构:用户→角色→资源的三层映射

RBAC 模式在实际项目中的层级结构清晰,通常分为三层,对应数据库表设计与程序逻辑:

层级 核心作用 数据库表 / 程序组件 示例
用户层 系统访问主体 sys_user 表 / UserDetails 接口 用户 adminalice
角色层 权限的集合载体,关联用户与资源 sys_role 表 / GrantedAuthority 角色 ROLE_ADMINROLE_USER
资源层 系统中需控制访问的对象(URL / 方法) 权限配置(Web 层 / 方法级注解) URL /admin/**、方法 deleteUser()

映射关系

  • 用户与角色:多对多(一个用户可拥有多个角色,一个角色可分配给多个用户),通过 sys_user_role 关联表实现;
  • 角色与资源:多对多(一个角色可访问多个资源,一个资源可允许多个角色访问),通过配置文件或数据库关联表实现。

从数据库设计到权限生效

以 “用户管理系统” 为例,完整实现 RBAC 模式的权限控制,包含数据库设计、用户详情加载、Web 层与方法级授权配置。

  1. 数据库表设计(RBAC 基础三表 + 关联表)
    表名 核心字段 示例数据
    sys_user(用户表) idusernamepassword 1, admin, 1234562, alice, 123456
    sys_role(角色表) idrole_name 1, ROLE_ADMIN2, ROLE_USER
    sys_user_role(用户 - 角色关联表) user_idrole_id 1,1(admin 关联 ADMIN 角色);2,2(alice 关联 USER 角色)
  2. 加载用户角色(自定义 UserDetailsService)
    通过 UserDetailsService 接口,从数据库加载用户信息时,同步加载用户所属角色,并封装为 GrantedAuthority 集合:
    @Service
    public class CustomUserDetailsService implements UserDetailsService {
        @Autowired
        private UserRepository userRepository;
        @Autowired
        private UserRoleRepository userRoleRepository;
        @Autowired
        private RoleRepository roleRepository;
    
        @Override
        public 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
            );
        }
    }
    
  3. 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_USERROLE_ADMIN 角色的用户访问。
  4. 方法级授权配置(业务方法级角色控制)
    通过 @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);
        }
    }
    

适用场景与局限性

  1. 适用场景
    RBAC 模式因其 “简单、易维护” 的特点,适合以下场景:
    • 中小型系统:用户量不大、角色类型少(如仅管理员、普通用户两类角色);
    • 权限逻辑稳定:角色与资源的关联关系长期不变(如管理员始终拥有所有权限,普通用户始终仅能访问个人资源);
    • 粗粒度权限控制:仅需按角色控制 URL 或业务方法的访问(无需精确到 “某用户可操作某条数据”)。
  2. 局限性
    当系统规模扩大或权限需求复杂时,RBAC 模式会暴露明显不足:
    • 权限过度授予:若一个角色包含多个权限,给用户分配该角色时,会自动获得所有权限,无法实现 “仅授予部分权限”(如普通用户需 “查看订单” 但无需 “导出订单”,但角色可能同时包含这两个权限);
    • 角色爆炸:当需要精细化控制时,需创建大量角色(如 ROLE_USER_VIEWROLE_USER_EDITROLE_USER_DELETE),导致角色数量激增,维护成本升高;
    • 无法满足数据级权限:无法控制 “用户只能访问自己创建的数据”(如普通用户仅能查看自己的订单,而非所有订单),需额外结合方法级校验(如 @PreAuthorize 中的数据归属判断)。

RBAC 模式的常见扩展
为弥补基础 RBAC 的局限性,实际项目中常对其进行简单扩展,平衡 “易用性” 与 “灵活性”:

  1. 角色分层(基础角色 + 附加角色)
    • 设计思路:将角色分为 “基础角色”(如 ROLE_USERROLE_ADMIN)和 “附加角色”(如 ROLE_ORDER_EXPORTROLE_DATA_VIEW);
    • 优势:基础角色保证核心权限,附加角色实现精细化权限补充,避免单一角色权限过度。
  2. 结合方法级数据校验
    • 针对 “数据级权限” 需求,在 RBAC 基础上,通过 @PreAuthorize 表达式添加数据归属校验:
      // 普通用户仅能修改自己的信息,管理员可修改所有用户
      @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
      public void updateUser(Long userId, UserDTO dto) {
          // 业务逻辑
      }
      

基于权限(PBAC)的访问控制

基于权限的访问控制(Permission-Based Access Control,简称 PBAC)是比 RBAC 更细粒度的权限设计模式。它跳过 “角色” 这一中间层,直接将 “权限” 与用户 / 资源绑定,核心是 “按操作许可控制访问”,而非 “按身份归类控制访问”。这种模式更适合中大型系统、操作类型多或权限需灵活调整的场景。

核心原理:权限作为 “操作许可” 的直接载体

PBAC 的核心思想是用 “权限” 描述 “用户可执行的具体操作”,直接建立 “用户 - 权限 - 资源” 的映射关系:

  • 定义细粒度的权限标识(如 user:read 表示 “查看用户”、user:delete 表示 “删除用户”),每个权限对应一个具体操作;
  • 给用户分配所需的权限(而非角色),用户拥有的权限集合决定了其可执行的操作;
  • 配置资源(URL / 方法)与权限的关联关系,只有拥有对应权限的用户才能访问资源。

这种模式的优势在于权限控制更精准—— 可按需给用户分配单个操作权限,避免 RBAC 中 “角色包含冗余权限” 的问题,严格遵循 “最小权限原则”。

层级结构:用户→权限→资源的三层映射

PBAC 模式的层级结构比 RBAC 更直接,去掉了 “角色” 中间层,聚焦 “操作许可” 与 “资源” 的绑定:

层级 核心作用 数据库表 / 程序组件 示例
用户层 系统访问主体 sys_user 表 / UserDetails 接口 用户 adminalice
权限层 具体操作的许可标识 sys_permission 表 / GrantedAuthority 权限 user:readuser:delete
资源层 系统中需控制访问的对象(URL / 方法) 权限配置(Web 层 / 方法级注解) URL /api/users(GET)、方法 deleteUser()

映射关系

  • 用户与权限:多对多(一个用户可拥有多个权限,一个权限可分配给多个用户),通过 sys_user_permission 关联表实现;
  • 权限与资源:多对多(一个权限可关联多个资源,一个资源可关联多个权限),通过配置文件或数据库表实现。

从数据库设计到权限生效

以 “用户管理系统” 为例,完整实现 PBAC 模式的权限控制,包含数据库设计、用户权限加载、Web 层与方法级授权配置。

  1. 数据库表设计(PBAC 核心三表 + 关联表)
    表名 核心字段 示例数据
    sys_user(用户表) idusernamepassword 1, admin, 1234562, alice, 123456
    sys_permission(权限表) idperm_codedescription 1, user:read, 查看用户2, user:delete, 删除用户
    sys_user_permission(用户 - 权限关联表) user_idperm_id 1,1(admin 有 user:read);1,2(admin 有 user:delete);2,1(alice 有 user:read)
  2. 加载用户权限(自定义 UserDetailsService)
    从数据库加载用户信息时,同步加载用户拥有的权限,封装为 GrantedAuthority 集合(权限标识直接作为 getAuthority() 返回值):
    @Service
    public class CustomUserDetailsService implements UserDetailsService {
        @Autowired
        private UserRepository userRepository;
        @Autowired
        private UserPermissionRepository userPermissionRepository;
        @Autowired
        private PermissionRepository permissionRepository;
    
        @Override
        public 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
            );
        }
    }
    
  3. Web 层授权配置(URL 级权限控制)
    SecurityFilterChain 中配置 URL 与权限的对应关系,控制不同权限的用户访问不同路径(尤其适合 REST API 按 HTTP 方法区分权限):
    @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();
    }
    
    按 “HTTP 方法 + URL” 组合配置权限,精准匹配 REST API 的 CRUD 操作(如 GET /api/users 对应 “查看” 权限,DELETE /api/users/** 对应 “删除” 权限),符合前后端分离项目的 API 设计习惯。
  4. 方法级授权配置(业务方法级权限控制)
    通过 @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 的扩展与最佳实践

  1. 权限命名规范
    为避免权限标识混乱,建议统一采用「资源:操作」的格式命名(如 user:readorder:create),其中:
    • 资源:对应业务实体(如 userorderdocument);
    • 操作:对应具体动作(如 readcreateupdatedeleteexport)。
  2. 结合角色简化权限分配(PBAC+RBAC 混合模式)
    在中大型系统中,纯 PBAC 会因权限数量多导致分配繁琐,可引入 “角色” 作为 “权限集合” 的载体,形成混合模式:
    • 定义 “角色” 关联一组权限(如 ROLE_USER 关联 user:readuser:updateROLE_ADMIN 关联所有权限);
    • 给用户分配角色(快速获得基础权限),同时支持给用户单独追加特殊权限(如给某用户额外分配 user:export);
    • 授权校验时,同时校验角色和权限(如 @PreAuthorize("hasRole('USER') and hasAuthority('user:export')"))。
  3. 权限的动态管理
    对于权限频繁变更的系统,建议将 “权限 - 资源” 的关联关系存储在数据库中(如 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(资源表) idurlhttp_methodname 存储系统中的资源(URL)信息 1, /api/users, GET, 用户列表查询2, /api/users/**, DELETE, 用户删除
sys_permission(权限表) idperm_codedescription 存储权限标识(如 PBAC 中的操作许可) 1, user:read, 查看用户2, user:delete, 删除用户
sys_resource_permission(资源 - 权限关联表) resource_idpermission_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/users POST 需 user:createrole:admin),一个权限可对应多个 URL(如 user:read 对应 /api/users GET 和 /api/user/{id} GET)。

从数据库加载权限规则

实现动态权限的关键步骤是「查询数据库中的资源 - 权限映射,转换为 Spring Security 可识别的授权规则」,核心通过 SecurityMetadataService 服务封装加载逻辑。

  1. 定义实体类与数据访问层
    实体类 Resource.javaPermission.javaResourcePermission.java
    // 资源实体(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
    // 资源 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);
    }
    
  2. 封装权限加载服务 SecurityMetadataService
    该服务的核心作用是「查询数据库中的资源 - 权限映射,转换为 Spring Security 授权规则所需的 RequestMatcher(URL 匹配器)和 AuthorizationManager(权限管理器)」:
    @Service
    public class SecurityMetadataService {
        @Autowired
        private ResourceRepository resourceRepo;
        @Autowired
        private 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 {
    @Autowired
    private SecurityMetadataService securityMetadataService;
    @Autowired
    private CustomUserDetailsService userDetailsService;

    // 密码编码器(测试用,生产环境用 BCryptPasswordEncoder)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    // 认证提供者(加载用户权限)
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    // 核心:配置动态权限的 SecurityFilterChain
    @Bean
    public 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() 方法动态添加 RequestMatcherAuthorizationManager 的映射,完全替代静态配置。

核心原理:动态授权的执行流程

动态权限的执行流程与静态配置一致,核心差异在于 “授权规则的来源”(数据库 vs 硬编码),流程如下:

在这里插入图片描述

关键节点

  • 步骤 5:RequestMatcherDelegatingAuthorizationManagerLinkedHashMap 的存储顺序匹配 URL,确保 “精确 URL 优先于模糊 URL”(需在数据库中按优先级排序);
  • 步骤 7:AuthorizationManager 的校验逻辑与静态配置一致(如 hasAnyAuthority 检查用户是否拥有指定权限)。

注意事项与优化建议

  1. URL 匹配顺序问题
    • 数据库中存储的资源(URL)需按 “精确到模糊” 的顺序排序(如 /api/users/1 优先于 /api/users/**),否则模糊 URL 会优先匹配,导致精确 URL 的规则失效;
    • 优化方案:在 sys_resource 表中添加 sort 字段,查询时按 sort 升序排列,确保匹配顺序正确。
  2. 权限加载时机问题
    • 目前的实现是 “系统启动时加载一次权限”,若数据库中的权限规则修改,需重启服务才能生效;
    • 后续优化:结合 “动态刷新权限”(第六模块内容),实现 “权限修改后无需重启,实时生效”。
  3. 性能优化
    • 系统启动时加载权限规则仅执行一次,性能影响可忽略;
    • 若资源数量极大(如 thousands 级别),可在 SecurityMetadataService 中添加缓存(如 Caffeine、Redis),避免频繁查询数据库(但启动时加载通常无需缓存)。

动态刷新权限表

在动态权限设计中,仅实现 “启动时从数据库加载权限” 还不够 —— 实际业务中,管理员可能随时在后台修改权限规则(如新增 URL 权限、调整权限关联),此时需要系统在运行时动态刷新权限配置,无需重启服务

核心问题:为何需要动态刷新?

启动时加载权限的方案存在明显局限:

  • 若数据库中权限规则(如 sys_resource_permission 表的关联关系)发生变化,必须重启服务才能生效;
  • 对于生产环境的核心系统,频繁重启会导致服务中断,影响可用性。

动态刷新的目标是:当权限规则变更时,系统能自动(或手动触发)重新加载权限配置,新规则实时生效

刷新原理:替换授权管理器实例

Spring Security 的授权逻辑由 AuthorizationManager 驱动,动态刷新的核心是:当权限规则变更时,重新构建 AuthorizationManager 实例,并替换当前正在使用的旧实例

关键组件关系如下:

  • AuthorizationFilter 是拦截请求并执行授权的过滤器,它依赖 AuthorizationManager 进行权限判断;
  • 若能在运行时用新的 AuthorizationManager(基于最新权限规则构建)替换旧实例,就能实现权限的动态生效。

实现方案一:定时任务刷新(适合权限变更不频繁场景)

通过定时任务(如每 1 分钟)定期查询数据库,重新构建 AuthorizationManager,适合权限变更频率低、对实时性要求不高的场景。

  1. 用线程安全的容器管理授权管理器
    使用 AtomicReference 存储 AuthorizationManager 实例,确保多线程环境下的可见性和原子性:
    @Service
    public class DynamicAuthorizationManager {
        // 原子引用存储当前生效的授权管理器(线程安全)
        private final AtomicReference<RequestMatcherDelegatingAuthorizationManager> currentAuthManager = new AtomicReference<>();
    
        @Autowired
        private SecurityMetadataService securityMetadataService;
    
        // 初始化:系统启动时加载首次权限
        @PostConstruct
        public 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();
        }
    }
    
  2. 配置定时任务触发刷新
    使用 Spring 的 @Scheduled 注解,定时执行 refresh() 方法:
    @Configuration
    @EnableScheduling // 启用定时任务
    public class ScheduledConfig {
        @Autowired
        private DynamicAuthorizationManager dynamicAuthManager;
    
        // 每 60 秒刷新一次权限(可根据业务调整频率)
        @Scheduled(fixedDelay = 60000)
        public void schedulePermissionRefresh() {
            dynamicAuthManager.refresh();
            log.info("定时刷新权限规则完成");
        }
    }
    
  3. 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();
    }
    

实现方案二:事件驱动刷新(适合权限实时变更场景)

当权限规则在数据库中发生变更时(如管理员在后台点击 “保存” 按钮),通过 “事件发布 - 订阅” 机制立即触发刷新,适合对实时性要求高的场景。

  1. 定义权限变更事件
    // 权限变更事件:当权限规则修改时发布
    public class PermissionChangedEvent extends ApplicationEvent {
        public PermissionChangedEvent(Object source) {
            super(source);
        }
    }
    
  2. 发布事件(在权限修改接口中触发)
    在修改权限的 Service 或 Controller 中,当权限规则保存成功后,发布 PermissionChangedEvent
    @Service
    public class PermissionService {
        @Autowired
        private ApplicationEventPublisher eventPublisher;
        @Autowired
        private ResourcePermissionRepository resourcePermissionRepo;
    
        // 修改资源-权限关联关系
        @Transactional
        public void updateResourcePermissions(Long resourceId, List<Long> permIds) {
            // 1. 删除旧关联
            resourcePermissionRepo.deleteByResourceId(resourceId);
            // 2. 保存新关联(省略具体逻辑)
            // ...
            // 3. 发布权限变更事件
            eventPublisher.publishEvent(new PermissionChangedEvent(this));
        }
    }
    
  3. 订阅事件并触发刷新
    DynamicAuthorizationManager 实现 ApplicationListener 接口,监听事件并刷新权限:
    @Service
    public class DynamicAuthorizationManager implements ApplicationListener<PermissionChangedEvent> {
        // 复用方案一中的 currentAuthManager、init()、refresh() 方法...
    
        // 监听权限变更事件,立即刷新
        @Override
        public void onApplicationEvent(PermissionChangedEvent event) {
            refresh();
            log.info("接收到权限变更事件,已刷新权限规则");
        }
    }
    
    事件发布机制可结合消息队列(如 RabbitMQ、Kafka)实现分布式系统的权限同步 —— 多实例部署时,一个实例修改权限后,通过消息队列通知所有实例刷新权限。

线程安全与性能优化

动态刷新涉及多线程操作(如定时任务线程、请求处理线程),需重点关注线程安全和性能问题:

  1. 线程安全保障
    • 使用 AtomicReference 存储 AuthorizationManager 实例,确保替换操作的原子性;
    • RequestMatcherDelegatingAuthorizationManager 本身是线程安全的(无状态设计),可在多线程中共享使用。
  2. 性能优化
    • 刷新时避免阻塞请求refresh() 方法构建新授权管理器的过程(查询数据库、构建规则)可能耗时,需异步执行:
      @Async // 异步执行刷新,不阻塞事件发布线程
      public void refresh() {
          // 构建新授权管理器的逻辑...
      }
      
    • 缓存数据库查询结果:若 loadResourcePermissions() 方法查询数据库耗时,可添加本地缓存(如 Caffeine),减少数据库压力:
      // 在 SecurityMetadataService 中添加缓存
      @Cacheable(value = "resourcePermissions", key = "'all'")
      public Map<RequestMatcher, AuthorizationManager<HttpServletRequest>> loadResourcePermissions() {
          // 数据库查询逻辑...
      }
      
      注意:缓存需在刷新前手动清除(@CacheEvict),确保加载最新数据。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐