开源仓库: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:update
sys:user:delete

sys:role:view

monitor:server

用户和角色多对多,角色和菜单多对多,菜单关联权限标识。这样用户 → 角色 → 菜单 → 权限,整条链路就通了。

数据库设计

核心表结构:

用户-角色关联

用户-角色关联

角色-菜单关联

角色-菜单关联

父子关系

sys_user

bigint

id

PK

varchar

username

varchar

password

bigint

dept_id

tinyint

status

sys_user_role

bigint

user_id

FK

bigint

role_id

FK

sys_role

bigint

id

PK

varchar

name

varchar

code

tinyint

data_scope

tinyint

status

sys_role_menu

bigint

role_id

FK

bigint

menu_id

FK

sys_menu

bigint

id

PK

varchar

name

varchar

path

varchar

perm

权限标识

tinyint

type

1目录 2菜单 3按钮

bigint

parent_id

权限标识命名规范:

格式:模块:资源:操作
示例:sys:user:create(系统模块-用户资源-新增操作)
通配:sys:user:*(用户资源下所有操作)
超管:*:*:*(超级管理员标识)

前后端权限协作架构

这是整个方案的核心,搞清楚数据怎么流转:

后端

前端

1. 登录
2. 返回 Token
(含 roles)
3. 获取用户信息
4. 返回 perms
5. 存储
6. 校验

读取

未命中

登录

/me 接口

Pinia Store
(roles + perms)

v-has-perm 指令

认证服务

JWT Token
(含 roles)

Redis 缓存
(角色权限)

数据库

权限数据流转

阶段 数据 存储位置
登录成功 roles(角色编码列表) JWT Token
调用 /me perms(权限标识列表) Pinia Store
权限校验 roles + perms 前端内存

为什么 JWT 存角色、/me 返回权限?

对比项 JWT 存权限 JWT 存角色
Token 大小 较大(权限多) 较小(角色少)
权限变更感知 需重新登录 调用 /me 即可刷新
实时性
安全性 Token 泄露暴露权限 Token 泄露仅暴露角色

结论:JWT 存角色,权限通过 /me 接口按需获取,兼顾性能与实时性。

后端接口鉴权

鉴权流程

Redis/DB @ss.hasPerm SecurityContext TokenFilter 客户端 Redis/DB @ss.hasPerm SecurityContext TokenFilter 客户端 alt [有权限] [无权限] 请求 + Token 解析 Token 存储用户信息(roles) @PreAuthorize("@ss.hasPerm") 获取角色权限(带缓存) 返回 perms 权限匹配(支持通配符) 执行业务逻辑 403 A0301

接口权限配置

@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。

前端 普通用户 Redis 后端 管理员 前端 普通用户 Redis 后端 管理员 修改角色权限 清除权限缓存 修改成功 操作按钮 请求接口 获取最新权限 返回新权限 权限不足 A0301 reloadPermissionSnapshotOnce() GET /me 返回最新 perms 更新 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 这套方案的亮点:

  1. JWT 存角色、/me 返回权限:兼顾 Token 大小和实时性
  2. 通配符权限匹配sys:user:* 匹配所有用户操作
  3. 权限变更实时生效:拦截错误码自动刷新,无需重新登录
  4. 前端指令化v-has-perm 一行搞定按钮权限
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐