当前位置: 首页 > news >正文

【Spring Security】授权(四)

Spring Security

  • 授权(Authorization)
    • 基于角色与基于权限的设计模式
      • 基本概念
      • 理解角色与权限
      • 基于角色(RBAC)的访问控制
      • 基于权限(PBAC)的访问控制
      • 动态权限设计
      • 动态刷新权限表

授权(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_ 前缀)+ 权限(自定义格式)统一封装为 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;}
    }
    
  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:
      idusernamepassword
      1admin123456
      2alice123456
    • sys_role:
      idrole_name
      1ROLE_ADMIN
      2ROLE_USER
    • sys_permission:
      idperm_codedescription
      1user:read查看用户
      2user:delete删除用户
    • sys_user_role:
      user_idrole_id
      11
      22
    • sys_role_permission:
      role_idperm_id
      11
      12
      21
  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) {// 拼接前缀后调用 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_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(用户表)idusernamepassword1, admin, 1234562, alice, 123456
    sys_role(角色表)idrole_name1, ROLE_ADMIN2, ROLE_USER
    sys_user_role(用户 - 角色关联表)user_idrole_id1,1(admin 关联 ADMIN 角色);2,2(alice 关联 USER 角色)
  2. 加载用户角色(自定义 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);}
    }
    
  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(用户表)idusernamepassword1, admin, 1234562, alice, 123456
    sys_permission(权限表)idperm_codedescription1, user:read, 查看用户2, user:delete, 删除用户
    sys_user_permission(用户 - 权限关联表)user_idperm_id1,1(admin 有 user:read);1,2(admin 有 user:delete);2,1(alice 有 user:read)
  2. 加载用户权限(自定义 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);}
    }
    
  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 {@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() 方法动态添加 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<>();@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();}
    }
    
  2. 配置定时任务触发刷新
    使用 Spring 的 @Scheduled 注解,定时执行 refresh() 方法:
    @Configuration
    @EnableScheduling // 启用定时任务
    public class ScheduledConfig {@Autowiredprivate 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 {@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));}
    }
    
  3. 订阅事件并触发刷新
    DynamicAuthorizationManager 实现 ApplicationListener 接口,监听事件并刷新权限:
    @Service
    public class DynamicAuthorizationManager implements ApplicationListener<PermissionChangedEvent> {// 复用方案一中的 currentAuthManager、init()、refresh() 方法...// 监听权限变更事件,立即刷新@Overridepublic 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),确保加载最新数据。
http://www.dtcms.com/a/526610.html

相关文章:

  • 网站设置了权限建立企业网站的目的和意义
  • 网站机房建设成本制作网页导航栏的步骤
  • 怎么盗号网站怎么做文件管理系统
  • 电子商务网站建设 教学ppt社交媒体平台
  • 包装设计网站是什么样子的什么是新媒体运营
  • 烟台网站建设 共赢为企业做网站的公司
  • 英文网站seo方案互联网公司的经营范围有哪些
  • 什么是网站关键词网站模板自助
  • 点击图片是网站怎么做的seo免费诊断联系方式
  • 网站备案 万网北京公司网站设计电话
  • 南通网站设计制作wordpress商品按钮代码
  • 保定市网站建设公司网站建设南宁
  • wordpress做教育网站锦州网站开发建设
  • 外贸人常用网站佛山宣传片制作公司
  • 网站的数据运营怎么做互联网公司网站建设ppt模板下载
  • 深圳网站建设公司排名seo站外推广有哪些
  • 北京做网站优化网站网站建设网页设计
  • 39、逻辑卷常见问题
  • 海口cms模板建站html格式的网站地图
  • 贵州网站制作设计公司wordpress jsp版
  • 做百度手机网站快速排工业和信息化部网站备案查询
  • 望江县建设局网站化州网站建设公司
  • 大连网站建设 仟亿新手建立网站的步骤
  • 建设网站怎么输入分子式陈木胜个人资料
  • CentOS7.9宝塔安装PostgreSQL及ltree
  • 数据库第一次练习
  • 连山建设局网站宁波seo网络推广推荐
  • 想要黑掉一个网站 要怎么做曹县做网站
  • wordpress 5.0多站点石家庄制作网站推广
  • 谷歌登录突然失效且错误码为12500