第3章:系统架构设计——权限系统的整体架构与数据流向
搞清楚了角色和权限的建模方式,接下来就该动真格的了。怎么把这些模型塞进一个真正的系统里?前端怎么显示、后端怎么验证、数据库怎么存,这三者得配合默契才行。我个人习惯先把整体架构立起来,再谈细节。
先看一张图,把权限系统的三个大块——前端、后端、数据库,以及它们之间的数据流转,一次说清楚。
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 请求,在权限模块里会经过三个关键节点:
- 拦截器 / 中间件:拿到请求头里的 JWT ,反解出用户 ID 和角色信息。 过期或解析失败,直接返回 401。
- 授权管理器:这里的核心是 AOP 切面。我习惯配上 Spring Security 的
@PreAuthorize注解,或者自己写一个校验切面,根据接口的权限标识去匹配用户的权限列表。 - 权限引擎:这是 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 完整交互流程:一次请求的权限之旅
把前端、后端、数据库串起来,一次带权限的请求长这样:
- 用户操作:点击“新增用户”按钮。
- 前端校验:
v-permission指令先检查本地权限列表,有user:add才发起请求。 - 后端拦截器:拿到 JWT ,反解析出 user_id 和 role_id。
- 权限引擎:根据 user_id,先去 Redis 查权限集合;没有的话回查 MySQL 的关联表。
- 授权校验:
@PreAuthorize("hasPermission('user:add')")对比当前操作的标识user:add是否在权限集合里。 - 数据操作:权限通过,执行业务逻辑,写入数据库。
- 返回结果:操作成功或失败的 JSON 响应给前端。
- 前端反馈:根据响应结果刷新 UI,显示成功提示或错误信息。
留意一下:第 2 步和第 5 步是重复校验。前端防君子,后端防小人。我曾经见过线上系统去掉了前端校验,结果后台操作延迟飙到 500ms,因为所有请求都在后端走了全链路权限查询。所以能前置到前端拦截的,务必拦截掉。
3.5 架构设计中的几个现实坑
- 权限数据不一致:用户角色变了,但前端还显示着老角色的菜单。解决方案:后端在权限变更时,主动清除该用户的 Redis 缓存和前端状态。我习惯用 WebSocket 通知前端刷新。
- 性能瓶颈:权限表一多,联表查询就很慢。我记得有次优化,把 5 张表的 JOIN 查询改成单次查询后,再在代码里做内存关联,响应时间从 800ms 降到了 50ms。
- 权限穿透:如果后端忘记加
@PreAuthorize,接口就是裸奔的。我建议加个全局扫描脚本,上线前自动检查所有 API 接口是否有权限注解。
嗯,架构设计说到这里。核心思路就是:前端控展示、后端控逻辑、数据库和缓存控数据。三者各司其职,权限系统才不会乱。