第七章 基于资源权限码(Authority)的接口权限控制实战
本章完成从"用户-角色-权限资源"数据模型到
@PreAuthorize方法级拦截的完整闭环。和上一章"角色控制(Role)"不同,本章重点是 资源权限码(Authority) ,即clue:list、clue:edit这类细粒度权限。你将得到一套能直接用于企业项目的权限控制方案,同时规避Controller 未注册、Mapper SQL 字段写错、权限码字段映射错位等高频坑位。
一、问题切入:为什么从角色升级到资源权限
在 RBAC 的第一阶段,我们常用 hasRole('ADMIN') 做控制,这对"大权限分层"足够,但在真实业务里会出现三个痛点:
- 同一角色下能力差异大:同是运营角色,有人可导出、有人只能查看
- 接口粒度需要更细 :
/api/clue/list、/api/clue/edit、/api/clue/del权限应独立 - 角色扩展会爆炸:用角色拼业务能力会导致角色数量快速膨胀
因此,本章采用更细粒度的控制方式:
- 角色负责"人群分组"
- 权限码(Authority)负责"具体动作授权"
也就是:用户 -> 角色 -> 资源权限码 -> 接口拦截。
二、权限模型与调用链
从项目结构看,本章已经具备"权限码驱动授权"的核心骨架:
Users实现UserDetails,并在getAuthorities()返回权限集合PermissionsMapper.selectByUserId负责查出用户拥有的权限ClueController使用@PreAuthorize("hasAuthority('...')")做方法级权限控制SecurityConfig已开启@EnableMethodSecurity
整体链路如下:
rust
登录 -> UserServiceImpl.loadUserByUsername()
-> 查询用户 permissionsList
-> Users.getAuthorities() 转成 GrantedAuthority
-> 放入 Authentication
-> 调用接口时 @PreAuthorize(hasAuthority(...)) 匹配
只要这条链每一环都打通,方法级鉴权就能稳定生效。
三、核心代码解读(按实际项目)
3.1 开启方法级权限拦截
SecurityConfig 上的配置是关键总开关:
less
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
// ...
}
没有这个注解,@PreAuthorize 不会生效。
同时项目里保留了通用登录校验:
scss
.authorizeHttpRequests((authorizeHttpRequests) ->{
authorizeHttpRequests
.requestMatchers("/tologin", "/common/captcha").permitAll()
.anyRequest().authenticated();
})
意思是:先保证"必须登录",再在方法层做"是否有某个资源权限码"。
3.2 用户权限装载:从数据库到 GrantedAuthority
UserServiceImpl 在登录时装载用户权限:
ini
List<Permissions> permissionsList = permissionsMapper.selectByUserId(users.getId());
users.setPermissionsList(permissionsList);
return users;
Users#getAuthorities() 再把 permissionsList 转为 Spring Security 可识别的权限集合:
csharp
for (Permissions permissions : this.permissionsList) {
authorities.add(new SimpleGrantedAuthority(permissions.getCode()));
}
注意这里的关键点:传给 SimpleGrantedAuthority 的必须是最终参与匹配的权限码字符串。
3.3 方法级权限控制写法
ClueController 采用资源权限码做拦截:
kotlin
@PreAuthorize("hasAuthority('clue:list')")
@RequestMapping("/api/clue/list")
public String clueList(){
return "clueList";
}
@PreAuthorize("hasAnyAuthority('clue:export','clue:download')")
@RequestMapping("/api/clue/export")
public String clueExport(){
return "clueExport";
}
这套写法比 hasRole 更细,能够直接落到业务动作级别。
四、ss07 当前实现里的关键坑位(必须修)
下面这些是我根据你的 ss07 源码直接定位出的实战问题。
4.1 ClueController 缺少 @Controller 注解
当前类定义只有:
kotlin
public class ClueController {
没有 @Controller 或 @RestController,Spring 不会注册该 Bean,@PreAuthorize 自然也不会触发。
建议改为:
kotlin
@Controller
public class ClueController {
4.2 Users.getAuthorities() 使用了不存在的字段 permissions.getCode()
Permissions 实体字段是:
permKeypermNamepermTypepathmethod
并没有 code 字段,因此当前代码会编译失败或运行异常。应改为:
csharp
authorities.add(new SimpleGrantedAuthority(permissions.getPermKey()));
并保证数据库 perm_key 存的就是 clue:list、clue:edit 等权限码。
4.3 PermissionsMapper.xml SQL 有别名与字段拼写错误
当前 SQL 片段:
csharp
from permissions
left join role_permissions rp on p.id = rp.premission_id
问题有两个:
permissions没有起别名p,却用了p.idpremission_id疑似拼写错误(常见应为permission_id)
建议修正示例:
csharp
select p.*
from permissions p
left join role_permissions rp on p.id = rp.permission_id
left join roles r on rp.role_id = r.id
left join user_roles ur on r.id = ur.role_id
where ur.user_id = #{id}
4.4 LoginInfoUtil 强转有风险
当前实现直接:
kotlin
return (Users) authentication.getPrincipal();
未登录或匿名场景会触发类型转换异常。建议加空值和类型判断。
五、推荐的"资源权限控制"标准写法
5.1 数据层建议
permissions 表建议至少包含:
perm_key:权限码(如clue:list)perm_name:权限名称(如"线索查看")path、method:可选,用于审计或动态鉴权
并通过 role_permissions 建立角色到权限资源的关联。
5.2 代码层建议
- 登录阶段只做一次权限装载
- 统一用
hasAuthority/hasAnyAuthority控制动作权限 - 接口命名和权限码保持一致规则(资源:动作)
示例:
makefile
clue:list
clue:view
clue:add
clue:edit
clue:del
clue:export
5.3 命名规范建议
- 推荐统一:
模块:动作 - 动作用有限集合:
list/view/add/edit/del/export/approve - 不要混用大小写,统一小写,降低误配风险
六、与角色控制(Role)的区别与配合
维度
Role(角色)
Authority(资源权限码)
粒度
粗
细
典型表达式
hasRole('ADMIN')
hasAuthority('clue:edit')
适用场景
模块级入口、菜单级隔离
接口级动作控制
是否自动加前缀
hasRole 自动补 ROLE_
hasAuthority 不补前缀
最佳实践是二者结合:
- URL 层做基础登录和大粒度限制
- 方法层做资源动作级控制
七、快速自测清单
- 修复
ClueController注解后,确认/api/clue/*能正确映射 - 登录普通用户,访问
@PreAuthorize("hasAuthority('clue:list')")接口,验证放行 - 同一用户访问
clue:del,验证 403 - 赋予该用户
clue:del权限后重新登录,验证可通过 - 打印
Authentication#getAuthorities(),确认权限码来自perm_key - 故意写错权限码(如
clue:delete),验证拦截行为符合预期
八、总结
本章的核心目标:基于资源权限码的精细化接口控制。关键不是写了多少注解,而是把"数据库权限码 -> UserDetails 权限集合 -> 方法拦截表达式"这条链打通并保持一致。
如果这条链任一处命名不一致、字段错位、Bean 未注册,权限看起来"写了但不生效"。反过来,一旦链路统一,你就能非常稳定地扩展到更多模块(客户、合同、回款、审批等),并把安全控制从"角色粗分"升级为"动作级可审计控制"。
编辑者 :Flittly
更新时间:2026年4月