角色与权限基础:角色定义、权限粒度与关联表设计
权限系统里,角色是个绕不开的核心概念。我见过不少团队一上来就撸代码,结果做到一半发现权限控制不住,回头再补角色设计——那叫一个痛苦。今天咱们就把角色、权限粒度、还有那张关键的关联表,一次性讲透。
角色到底是什么?
说白了,角色就是一组权限的集合。你想想看,一个企业里可能有几百号人,如果每个人单独配权限,那运维得疯掉。角色就是用来做「批量授权」的。
举个例子:
- 系统管理员:能看所有菜单、点所有按钮、调所有API
- 业务主管:能看到业务模块,能审批,但不能改系统配置
- 普通员工:只能看自己的数据,只能点查询按钮
我在项目中遇到过一种情况:客户说「我们不需要角色,直接给用户挂权限就行」。结果上线三个月,权限表膨胀到几十万行,每次查询都慢得要命。后来还是老老实实加了角色层。所以我的建议是——角色不是可选项,是必选项。
核心原则:角色是权限的容器,用户通过角色获得权限。不要绕过角色直接给用户赋权。
权限粒度:菜单、按钮、API
权限粒度,就是你能控制到多细。我习惯把粒度分成三个层次,每个层次解决不同的问题。
1. 菜单权限(第一层)
控制用户能看到哪些页面。比如财务模块只有财务人员能看到,销售看不到。这是最粗的粒度,也是用户体验的第一道门。
2. 按钮权限(第二层)
控制用户能操作哪些功能。比如同一个订单列表,有人能看到「新增」按钮,有人只能看到「查询」。这一层直接影响业务操作的安全性。
3. API权限(第三层)
控制后端接口的访问。这是最后一道防线。前端可以隐藏按钮,但懂行的用户可能直接调接口。API权限必须做,而且要做在网关层。
我曾经踩过一个坑:只做了菜单和按钮权限,觉得前端隐藏了就安全了。结果有个用户通过浏览器开发者工具直接调了删除接口……嗯,从那以后我再也不敢省略API权限了。
我的建议:菜单和按钮权限做在前端,API权限做在后端网关。前端控制体验,后端控制安全,缺一不可。
权限粒度对比表
| 粒度层级 | 控制对象 | 实现位置 | 典型示例 |
|---|---|---|---|
| 菜单权限 | 页面/路由 | 前端路由守卫 | 财务模块只有财务角色可见 |
| 按钮权限 | 操作/功能 | 前端指令/组件 | 只有主管能看到「审批」按钮 |
| API权限 | 接口/资源 | 后端网关/拦截器 | 非管理员无法调用删除接口 |
角色-权限关联表设计
这是整个权限系统的核心数据结构。设计得好,后面扩展起来很轻松;设计得不好,改一次表结构要动半套代码。
我常用的方案是「三张核心表 + 两张关联表」:
- 角色表 (role):存角色基本信息,比如角色名、角色编码、状态
- 权限表 (permission):存权限项,包括菜单、按钮、API,用type字段区分
- 用户表 (user):存用户信息
- 用户-角色关联表 (user_role):用户和角色是多对多关系
- 角色-权限关联表 (role_permission):角色和权限是多对多关系
这里重点说角色-权限关联表。它的结构其实很简单:
CREATE TABLE role_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_perm (role_id, permission_id)
);
为什么用多对多?因为一个角色可以有多个权限,一个权限也可以属于多个角色。比如「查询订单」这个权限,既可能挂在「销售员」角色上,也可能挂在「客服」角色上。
注意:关联表一定要加唯一索引 (role_id, permission_id),防止重复数据。我见过有人没加,结果权限配置页面点一次就多一条记录,最后查出来几万条重复数据。
完整的数据模型
把上面几张表串起来,就是一套完整的权限数据模型:
-- 角色表
CREATE TABLE role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '角色名称',
code VARCHAR(50) NOT NULL UNIQUE COMMENT '角色编码',
status TINYINT DEFAULT 1 COMMENT '1启用 0禁用',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 权限表
CREATE TABLE permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '权限名称',
code VARCHAR(100) NOT NULL UNIQUE COMMENT '权限编码',
type TINYINT NOT NULL COMMENT '1菜单 2按钮 3API',
parent_id BIGINT DEFAULT 0 COMMENT '父权限ID',
sort_order INT DEFAULT 0 COMMENT '排序',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 用户-角色关联表
CREATE TABLE user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_role (user_id, role_id)
);
-- 角色-权限关联表
CREATE TABLE role_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_perm (role_id, permission_id)
);
核心逻辑流程图
下面这张图展示了角色、用户、权限三者之间的关系:
权限编码设计的小技巧
权限编码(code字段)是个容易被忽略但很重要的设计。我习惯用层级编码,一眼就能看出这个权限属于哪个模块:
# 菜单权限
order:list # 订单列表页面
order:detail # 订单详情页面
finance:report # 财务报表页面
# 按钮权限
order:btn:add # 新增订单按钮
order:btn:edit # 编辑订单按钮
order:btn:delete # 删除订单按钮
# API权限
order:api:create # 创建订单接口
order:api:update # 更新订单接口
order:api:delete # 删除订单接口
这种编码方式的好处是:
- 可读性强,看到编码就知道是哪个模块的什么操作
- 方便做通配符匹配,比如
order:*表示订单模块所有权限 - 前端后端统一,减少沟通成本
小技巧:权限编码建议全小写,用冒号分隔层级。不要用数字ID做编码,那玩意儿可读性太差,排查问题的时候你会想骂人。
避坑指南
最后分享几个我踩过的坑:
- 不要用角色直接关联用户:如果用户和角色是直接关联,那角色就没法复用了。一定要用关联表。
- 权限表不要只存菜单:我见过有人把按钮和API权限写在代码里,结果每次改权限都要发版。权限表应该覆盖所有粒度。
- 关联表一定要加索引:用户量上来之后,关联表查询频率很高,不加索引就是给自己挖坑。
- 权限编码一旦确定不要轻易改:改编码意味着要同步修改前端、后端、数据库,牵一发动全身。
嗯,角色和权限的基础就这些。把这些设计想清楚,后面的行列权限模型才能站得住脚。