第3章:系统架构设计——权限系统的整体架构与数据流向

搞清楚了角色和权限的建模方式,接下来就该动真格的了。怎么把这些模型塞进一个真正的系统里?前端怎么显示、后端怎么验证、数据库怎么存,这三者得配合默契才行。我个人习惯先把整体架构立起来,再谈细节。

先看一张图,把权限系统的三个大块——前端、后端、数据库,以及它们之间的数据流转,一次说清楚。

权限系统整体架构与数据流向图 权限系统整体架构与数据流向 前端层 Vue / React 组件 v-if 指令 / 按钮级隐藏 路由守卫 权限拦截 / 跳转登录页 权限指令 (v-permission) 自定义指令封装 UI 状态管理 Pinia / Vuex 缓存权限 API 请求封装 后端层 拦截器 / 中间件 JWT 令牌校验 权限校验核心 RBAC 模型引擎 授权管理器 @PreAuthorize / AOP 业务逻辑层 Service 注入 ORM 持久化层 数据层 关系型数据库 (MySQL) 核心权限表 + 字段 Redis 缓存 用户授权数据缓存 实体映射 (JPA/MyBatis) PO / DTO 转换 SQL / 存储过程 权限树查询 请求 带 查询 ORM 缓存 / 返回 响应 权限数据 交互核心 前端请求 → 后端拦截器校验 → 权限引擎匹配 RBAC 模型 → 数据库 / 缓存返回 → 后端响应 → 前端渲染

3.1 前端层:我怎么设计权限相关的UI逻辑

前端不是只管展示好看,它得承担权限的“第一道防线”。说白了,用户能看到什么按钮、能点进哪个页面,全靠前端控制。

我在项目中通常分三层来处理:

  • 路由级控制:用户登录后,动态生成可访问的路由表。没权限的菜单直接隐藏,不会出现在侧边栏。
  • 视图级控制:同一个页面里,管理员能看到“删除”按钮,普通用户只看到“查看”。我习惯用自定义指令 v-permission 包裹。
  • API 级兜底:前端隐藏了按钮,不代表后端不校验。前端只是提升用户体验的手段,真正的安全感在后端。

关键设计点:前端权限状态建议用 Pinia 或 Vuex 统一管理。登录成功后,后端一次性返回当前用户的权限列表(比如:['user:add', 'user:delete']),前端解析后存入状态树。

// 伪代码:自定义权限指令
app.directive('permission', {
  mounted(el, binding) {
    const userPermissions = usePermissionStore().getters.permissions
    if (!userPermissions.includes(binding.value)) {
      el.parentNode?.removeChild(el) // 无权限 → 直接销毁DOM
    }
  }
})

我的经验:千万别把权限判断全部写在组件内部 v-if="role === 'admin'"。一旦角色增加,代码就变成一坨。封装成指令或组件,后期改起来轻松很多。

3.2 后端层:权限校验的硬核心脏

后端是权限系统的真正战场。前端只是“面子”,后端才是“里子”。

一次完整的 API 请求,在权限模块里会经过三个关键节点:

  1. 拦截器 / 中间件:拿到请求头里的 JWT ,反解出用户 ID 和角色信息。 过期或解析失败,直接返回 401。
  2. 授权管理器:这里的核心是 AOP 切面。我习惯配上 Spring Security 的 @PreAuthorize 注解,或者自己写一个校验切面,根据接口的权限标识去匹配用户的权限列表。
  3. 权限引擎:这是 RBAC 模型的运行时实现。输入是“用户ID + 资源标识”,输出是“有/无权限”。速度快不快,全看这里。

避坑指南:我曾经遇到过一个线上事故——某个接口只加了拦截器校验,没加注解校验,结果用户通过直接调用 API 绕过了前端限制。从那以后,我坚持“双重校验”:拦截器做通用鉴权,注解做接口级精确控制。

3.3 数据层:表结构与缓存协同

数据库设计是架构的基础。我常用的权限核心表有5张:

表名 核心字段 说明
sys_user user_id, role_id, status 用户表,关联角色
sys_role role_id, role_name, role_key 角色表,如 admin、user
sys_permission perm_id, perm_code, perm_name 资源表,定义每个可控制的功能点
sys_role_perm role_id, perm_id 角色-权限关联表
sys_user_role user_id, role_id 用户-角色关联表(支持多角色)

你想想看,如果每次请求都去 MySQL 查一遍权限,并发一上来就直接打挂了。所以我通常会加一层 Redis 缓存:用户登录成功后,把他的权限数据序列化成 JSON 缓存起来,过期时间设为 2 小时。缓存的 key 规则很简单:auth:user:{userId}:permissions。

// 缓存策略示例(Spring Boot + Redis)
public List<String> getUserPermissions(Long userId) {
    String cacheKey = "auth:user:" + userId + ":permissions";
    // 先查缓存
    List<String> perms = redisTemplate.opsForList().range(cacheKey, 0, -1);
    if (perms != null && !perms.isEmpty()) {
        return perms;
    }
    // 缓存未命中 → 查数据库 → 回填缓存
    perms = userMapper.selectPermissionsByUserId(userId);
    if (perms != null && !perms.isEmpty()) {
        redisTemplate.opsForList().rightPushAll(cacheKey, perms);
        redisTemplate.expire(cacheKey, 2, TimeUnit.HOURS);
    }
    return perms;
}

3.4 完整交互流程:一次请求的权限之旅

把前端、后端、数据库串起来,一次带权限的请求长这样:

  1. 用户操作:点击“新增用户”按钮。
  2. 前端校验:v-permission 指令先检查本地权限列表,有 user:add 才发起请求。
  3. 后端拦截器:拿到 JWT ,反解析出 user_id 和 role_id。
  4. 权限引擎:根据 user_id,先去 Redis 查权限集合;没有的话回查 MySQL 的关联表。
  5. 授权校验:@PreAuthorize("hasPermission('user:add')") 对比当前操作的标识 user:add 是否在权限集合里。
  6. 数据操作:权限通过,执行业务逻辑,写入数据库。
  7. 返回结果:操作成功或失败的 JSON 响应给前端。
  8. 前端反馈:根据响应结果刷新 UI,显示成功提示或错误信息。

留意一下:第 2 步和第 5 步是重复校验。前端防君子,后端防小人。我曾经见过线上系统去掉了前端校验,结果后台操作延迟飙到 500ms,因为所有请求都在后端走了全链路权限查询。所以能前置到前端拦截的,务必拦截掉。

3.5 架构设计中的几个现实坑

  • 权限数据不一致:用户角色变了,但前端还显示着老角色的菜单。解决方案:后端在权限变更时,主动清除该用户的 Redis 缓存和前端状态。我习惯用 WebSocket 通知前端刷新。
  • 性能瓶颈:权限表一多,联表查询就很慢。我记得有次优化,把 5 张表的 JOIN 查询改成单次查询后,再在代码里做内存关联,响应时间从 800ms 降到了 50ms。
  • 权限穿透:如果后端忘记加 @PreAuthorize,接口就是裸奔的。我建议加个全局扫描脚本,上线前自动检查所有 API 接口是否有权限注解。

嗯,架构设计说到这里。核心思路就是:前端控展示、后端控逻辑、数据库和缓存控数据。三者各司其职,权限系统才不会乱。