【Spring Boot实战系列】接口鉴权怎么做?RBAC权限模型前后端协作实战
·
开源仓库:youlai-boot

前言
本系列文章基于 youlai-boot 开源项目实践,一个开箱即用的 Spring Boot 后台管理系统。
权限这块,很多人会做后端鉴权,接口加个 @PreAuthorize 就完事。但前端按钮权限呢?不同角色看到不同按钮,这个怎么搞?
还有个问题:管理员改了用户权限,用户那边还在操作,权限怎么能实时生效?
这两个问题,youlai-boot 都解决了。今天就来聊聊 RBAC 权限模型的前后端协作方案。
RBAC 模型:用户-角色-权限
RBAC(Role-Based Access Control)是最经典的权限模型,核心就四个概念:
| 概念 | 说明 |
|---|---|
| 用户(User) | 系统操作者,通过角色间接获得权限 |
| 角色(Role) | 权限的集合,用户与权限的桥梁 |
| 菜单(Menu) | 系统功能入口,关联权限标识 |
| 权限标识(Perm) | 功能权限的唯一标识,如 sys:user:create |
权限层级关系:
用户和角色多对多,角色和菜单多对多,菜单关联权限标识。这样用户 → 角色 → 菜单 → 权限,整条链路就通了。
数据库设计
核心表结构:
权限标识命名规范:
格式:模块:资源:操作
示例:sys:user:create(系统模块-用户资源-新增操作)
通配:sys:user:*(用户资源下所有操作)
超管:*:*:*(超级管理员标识)
前后端权限协作架构
这是整个方案的核心,搞清楚数据怎么流转:
权限数据流转:
| 阶段 | 数据 | 存储位置 |
|---|---|---|
| 登录成功 | roles(角色编码列表) | JWT Token |
| 调用 /me | perms(权限标识列表) | Pinia Store |
| 权限校验 | roles + perms | 前端内存 |
为什么 JWT 存角色、/me 返回权限?
| 对比项 | JWT 存权限 | JWT 存角色 |
|---|---|---|
| Token 大小 | 较大(权限多) | 较小(角色少) |
| 权限变更感知 | 需重新登录 | 调用 /me 即可刷新 |
| 实时性 | 低 | 高 |
| 安全性 | Token 泄露暴露权限 | Token 泄露仅暴露角色 |
结论:JWT 存角色,权限通过 /me 接口按需获取,兼顾性能与实时性。
后端接口鉴权
鉴权流程
接口权限配置
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping
@PreAuthorize("@ss.hasPerm('sys:user:list')")
public Result<Page<User>> listUsers() { ... }
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:user:create')")
public Result<Void> addUser() { ... }
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:user:update')")
public Result<Void> updateUser() { ... }
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:user:delete')")
public Result<Void> deleteUser() { ... }
}
@ss 是 Spring Bean 名称,hasPerm 是方法名。SpEL 表达式调用这个方法做权限校验。
权限校验组件
@Component("ss")
public class PermissionService {
/**
* 判断当前用户是否拥有指定权限
* 支持通配符匹配:sys:user:* 可匹配 sys:user:create
*/
public boolean hasPerm(String requiredPerm) {
// 1. 超级管理员放行
if (SecurityUtils.isRoot()) {
return true;
}
// 2. 获取用户角色
Set<String> roleCodes = SecurityUtils.getRoles();
// 3. 获取角色权限(从缓存读取)
Set<String> perms = roleMenuService.getRolePermsByRoleCodes(roleCodes);
// 4. 权限匹配(支持通配符)
return perms.stream()
.anyMatch(perm -> PatternMatchUtils.simpleMatch(perm, requiredPerm));
}
}
权限缓存
每次请求都查数据库肯定不行。youlai-boot 用 Redis Hash 缓存角色权限:
Key: system:role:perms
Type: Hash
Field: ADMIN Value: ["sys:user:*", "sys:role:*", ...]
Field: USER Value: ["sys:user:list", ...]
缓存刷新时机:
| 触发场景 | 刷新方法 |
|---|---|
| 角色分配菜单 | refreshRolePermsCache(roleCode) |
| 菜单权限修改 | refreshRolePermsCache() 全量刷新 |
| 角色删除 | refreshRolePermsCache(roleCode) |
前端按钮权限
/me 接口响应
{
"code": "00000",
"data": {
"userId": 1,
"username": "admin",
"nickname": "管理员",
"roles": ["ADMIN", "USER"],
"perms": ["sys:user:list", "sys:user:create", "sys:user:update", "sys:user:delete"]
}
}
权限存储
// store/modules/user.ts
const userInfo = reactive({
roles: [] as string[],
perms: [] as string[], // 按钮权限列表
});
async function getUserInfo() {
const { data } = await UserAPI.getCurrentUserInfo();
userInfo.roles = data.roles;
userInfo.perms = data.perms;
}
自定义指令
// directives/permission/index.ts
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const requiredPerms = binding.value;
const { roles, perms } = useUserStore().userInfo;
// 超级管理员放行
if (roles.includes(ROLE_ROOT)) {
return;
}
// 检查权限
const hasAuth = Array.isArray(requiredPerms)
? requiredPerms.some((perm) => perms.includes(perm))
: perms.includes(requiredPerms);
// 无权限则移除元素
if (!hasAuth && el.parentNode) {
el.parentNode.removeChild(el);
}
},
};
使用方式
<template>
<!-- 单个权限 -->
<el-button v-has-perm="'sys:user:create'">新增</el-button>
<!-- 多个权限(满足其一即可) -->
<el-button v-has-perm="['sys:user:update', 'sys:user:delete']">操作</el-button>
<!-- 角色控制 -->
<el-button v-has-role="'ADMIN'">管理员操作</el-button>
</template>
权限变更实时生效
管理员改了用户权限,用户那边怎么实时生效?
方案:后端返回权限不足错误码 → 前端拦截后重新获取权限 → 更新 Store。
前端处理:
// request.ts - 拦截权限不足响应
if (code === ApiCodeEnum.PERMISSION_DENIED) {
await reloadPermissionSnapshotOnce();
// 重试原请求或跳转登录页
}
async function reloadPermissionSnapshotOnce(): Promise<void> {
await userStore.getUserInfo(); // 重新获取用户信息(含最新 perms)
}
这套方案的好处是权限变更后,用户下次操作时自动刷新,无需强制重新登录。
最佳实践
权限标识命名
// 推荐:标准三段式
sys:user:create // 系统模块-用户资源-新增操作
sys:role:delete // 系统模块-角色资源-删除操作
// 不推荐
userAdd // 缺少模块前缀
sys_user_add // 使用下划线(风格不统一)
接口权限配置
// 推荐:细粒度控制
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:user:list')")
public Result<User> getUser() { ... }
// 不推荐:过粗的权限
@GetMapping
@PreAuthorize("@ss.hasPerm('sys:user')")
public Result<Page<User>> listUsers() { ... }
前端权限控制
<!-- 推荐:指令控制 -->
<el-button v-has-perm="'sys:user:create'">新增</el-button>
<!-- 不推荐:v-if 硬编码 -->
<el-button v-if="perms.includes('sys:user:create')">新增</el-button>
结语
RBAC 权限模型本身不复杂,难的是前后端协作。youlai-boot 这套方案的亮点:
- JWT 存角色、/me 返回权限:兼顾 Token 大小和实时性
- 通配符权限匹配:
sys:user:*匹配所有用户操作 - 权限变更实时生效:拦截错误码自动刷新,无需重新登录
- 前端指令化:
v-has-perm一行搞定按钮权限
更多推荐




所有评论(0)