接口鉴权
youlai-boot 采用经典的 RBAC(Role-Based Access Control) 权限模型,实现用户-角色-权限的灵活访问控制,涵盖后端接口鉴权与前端按钮权限协作。
RBAC 模型
核心概念:
| 概念 | 说明 |
|---|---|
| 用户(User) | 系统操作者,通过角色间接获得权限 |
| 角色(Role) | 权限的集合,用户与权限的桥梁 |
| 菜单(Menu) | 系统功能入口,关联权限标识 |
| 权限标识(Perm) | 功能权限的唯一标识,如 sys:user:create |
权限层级:
数据库设计
核心表结构:
权限标识规范:
| 格式 | 示例 | 说明 |
|---|---|---|
模块:资源:操作 | sys:user:create | 标准三段式 |
模块:资源:* | sys:user:* | 资源下所有操作 |
*:*:* | *:*:* | 超级管理员标识 |
命名建议:
- 模块名:业务领域(sys、monitor、workflow)
- 资源名:数据实体(user、role、menu)
- 操作名:CRUD扩展(list、create、update、delete、export、import)
前后端权限协作
整体架构:
权限数据流转:
| 阶段 | 数据 | 存储位置 |
|---|---|---|
| 登录成功 | roles(角色编码列表) | JWT Token |
| 调用 /me | perms(权限标识列表) | Pinia Store |
| 权限校验 | roles + perms | 前端内存 |
为什么 JWT 存角色、/me 返回权限?
| 对比项 | JWT 存权限 | JWT 存角色 |
|---|---|---|
| Token 大小 | 较大(权限多) | 较小(角色少) |
| 权限变更感知 | 需重新登录 | 调用 /me 即可刷新 |
| 实时性 | 低 | 高 |
| 安全性 | Token 泄露暴露权限 | Token 泄露仅暴露角色 |
结论:JWT 存角色、权限通过 /me 接口按需获取,兼顾性能与实时性。
后端接口鉴权
鉴权流程:
SpEL 权限校验:
java
@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() { ... }
}权限校验组件:
java
@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));
}
}权限缓存机制:
缓存结构:
Key: system:role:perms
Type: Hash
Field: ADMIN Value: ["sys:user:*", "sys:role:*", ...]
Field: USER Value: ["sys:user:list", ...]
Field: GUEST Value: []缓存刷新时机:
| 触发场景 | 刷新方法 |
|---|---|
| 角色分配菜单 | refreshRolePermsCache(roleCode) |
| 菜单权限修改 | refreshRolePermsCache() 全量刷新 |
| 角色删除 | refreshRolePermsCache(roleCode) |
前端按钮权限
/me 接口响应:
json
{
"code": "00000",
"data": {
"userId": 1,
"username": "admin",
"nickname": "管理员",
"avatar": "https://...",
"roles": ["ADMIN", "USER"],
"perms": ["sys:user:list", "sys:user:create", "sys:user:update", "sys:user:delete"]
}
}权限存储:
typescript
// store/modules/user.ts
const userInfo = reactive({
userId: 0,
username: "",
roles: [] as string[],
perms: [] as string[], // 按钮权限列表
});
// 从 /me 接口获取后存储
async function getUserInfo() {
const { data } = await UserAPI.getCurrentUserInfo();
userInfo.roles = data.roles;
userInfo.perms = data.perms;
}自定义指令:
typescript
// 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);
}
},
};使用方式:
vue
<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>权限判断函数:
typescript
// utils/auth.ts
export function hasPerm(value: string | string[]): boolean {
const { roles, perms } = useUserStoreHook().userInfo;
// 超级管理员拥有所有权限
if (roles.includes(ROLE_ROOT)) {
return true;
}
return typeof value === "string"
? perms.includes(value)
: value.some((perm) => perms.includes(perm));
}
// 使用示例
if (hasPerm("sys:user:delete")) {
// 执行删除操作
}权限变更处理
变更通知机制:
前端处理:
typescript
// store/modules/permission.ts
async function reloadPermissionSnapshotOnce(): Promise<void> {
// 1. 重新获取用户信息(含最新 perms)
await userStore.getUserInfo();
}
// request.ts - 拦截权限不足响应
if (code === ApiCodeEnum.PERMISSION_DENIED) {
await reloadPermissionSnapshotOnce();
// 重试原请求或跳转登录页
}响应码定义:
后端文件:
| 文件 | 说明 |
|---|---|
security/service/PermissionService.java | 权限校验组件(@ss.hasPerm) |
system/service/RoleMenuService.java | 角色菜单服务(权限缓存管理) |
security/filter/TokenAuthenticationFilter.java | Token 认证过滤器 |
system/mapper/RoleMenuMapper.java | 角色-菜单关联查询 |
前端文件:
| 文件 | 说明 |
|---|---|
directives/permission/index.ts | 权限指令(v-has-perm) |
store/modules/user.ts | 用户状态管理(存储 perms) |
utils/auth.ts | 权限判断工具函数 |
最佳实践
权限标识命名:
java
// 推荐:标准三段式
sys:user:create // 系统模块-用户资源-新增操作
sys:role:delete // 系统模块-角色资源-删除操作
monitor:server // 监控模块-服务器资源
// 不推荐
userAdd // 缺少模块前缀
sys_user_add // 使用下划线(风格不统一)接口权限配置:
java
// 推荐:细粒度控制
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:user:list')")
public Result<User> getUser() { ... }
// 不推荐:过粗的权限
@GetMapping
@PreAuthorize("@ss.hasPerm('sys:user')")
public Result<Page<User>> listUsers() { ... }前端权限控制:
vue
<!-- 推荐:指令控制 -->
<el-button v-has-perm="'sys:user:create'">新增</el-button>
<!-- 不推荐:v-if 硬编码 -->
<el-button v-if="perms.includes('sys:user:create')">新增</el-button>超级管理员判断:
java
// 后端:ROOT 角色放行
if (SecurityUtils.isRoot()) {
return true;
}
// 前端:ROOT 角色拥有所有权限
if (roles.includes(ROLE_ROOT)) {
return true;
}相关文件
后端文件:
| 文件 | 说明 |
|---|---|
security/service/PermissionService.java | 权限校验组件(@ss.hasPerm) |
system/service/RoleMenuService.java | 角色菜单服务(权限缓存管理) |
security/filter/TokenAuthenticationFilter.java | Token 认证过滤器 |
system/mapper/RoleMenuMapper.java | 角色-菜单关联查询 |
前端文件:
| 文件 | 说明 |
|---|---|
directives/permission/index.ts | 权限指令(v-has-perm) |
store/modules/user.ts | 用户状态管理(存储 perms) |
utils/auth.ts | 权限判断工具函数 |
