接手后台管理系统多了,你会发现权限这块的坑几乎长得一模一样:前端把新增按钮藏起来就以为安全了,后端每个Service方法开头翻来覆去写 if ("admin".equals(...)),等到角色多起来,判断条件写成一长串不说,改一个角色的权限还得重新发版。这篇文章我会把我一直在用的一套组合方案完整梳理一遍——用 RBAC 做数据模型,用 SpringSecurity 做认证和登录态管理,再用自定义注解把接口权限声明收敛到一行代码上。三者各管一段,既不会把判断逻辑散落在业务代码里,也不会让后续加角色改权限变得畏手畏脚。如果你正打算给后台系统搭权限模块,或者想把现有那堆硬编码权限判断收拾干净,这篇应该可以作为一份落地参考。
1. 权限控制的乱象根源:藏在接口和业务代码里的硬编码
1.1 “前端隐藏按钮”不等于后端安全
很多项目第一个风险点,是把权限控制写在了页面上。用户登录后,前端根据角色对菜单和按钮做显隐控制,看起来用户看不到删除按钮,就不会删除数据。问题在于,前端隐藏只是体验优化,不是权限控制。只要用户已经登录,他发什么 HTTP 请求后端是拦不住的,删除接口如果只做登录校验,被从抓包记录里找到后手动调用一次,照样能把数据删掉。
要验证自己的系统是不是这样,方法很简单:用一个没有删除权限的普通账号登录,打开浏览器控制台,直接往删除接口发一个请求,看看能不能删成功。如果答案是能,说明接口层权限校验是缺失的。我之前处理过一个项目就是这种状态,菜单表、按钮权限表都建了,Controller 层却只判断了登录状态。后来一个不熟悉系统的运营,通过旧页面的请求记录调删除接口,把测试环境的配置误删了。没造成多大事故,但那次之后我给自己定了个原则:前端控制的是“能不能看到”,后端要控制的是“能不能做”,两者缺一不可,且后端必须兜底。
1.2 业务代码里到处是 isAdmin 判断
比接口裸奔更常见的,是每个 Service 方法开头都有这么一段:
java复制if (!"admin".equals(loginUser.getRoleCode())) {
throw new BusinessException("当前用户没有权限执行此操作");
}
需求简单的时候,这段代码没任何问题。但角色一旦多起来——除了管理员,还有运营、审核、财务、客服——你会发现不是所有操作都只有超管能做,而是运营能导出数据但不能删除,审核能通过订单但不能退款,财务能看金额但不能改金额。一个简单的 isAdmin 判断完全承载不了这种差异。
我见过最夸张的版本,一个方法里叠了三层角色判断:
java复制if (isAdmin || (isOp && !operation.equals("delete")) || (isAudit && operation.equals("view"))) {
// do something
}
这种代码改起来非常折磨人,因为你必须把每个角色在每个操作上的权限都读一遍才能下手。更头疼的是用户可能同时属于多个角色,只要逻辑稍微绕一点,极容易漏掉边界。走到这一步,问题已经不在某个 if 写得对不对,而在于系统根本没有把“权限”抽象成独立的数据模型。
1.3 痛点背后真正缺的是“权限模型”
把项目里反复出现的问题放在一起看,会发现本质不是代码写得不好,而是项目里没有一套清晰的权限模型。能不能访问某个菜单,能不能点某个按钮,能不能调用某个接口,这些事不应该散落在业务逻辑里反复判断,而应该被抽象成用户、角色、权限之间的数据关系。
RBAC 模型解决的就是这个问题。它把用户和权限中间加了一层角色,权限分配从“给每个用户单独授权”变成“把权限绑到角色上,再把用户放进角色”。拿门禁卡来类比:权限是一把把钥匙,角色是一个岗位钥匙包,用户是拿钥匙包的人。调整某人权限时,不需要一把把换钥匙,只需换掉整个钥匙包;调整一批同岗位员工的权限时,也只需要改角色对应的钥匙包。后面几个章节要做的,就是基于这套思路把数据表建出来,再把“当前用户是否具备当前操作的权限标识”这个校验逻辑收敛到统一位置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RBAC 数据模型落地方案:五张表和一个权限标识约定
2.1 RBAC 如何映射到管理后台的真实场景
RBAC 落到管理后台,核心关系就三句话:一个用户可以有多个角色,一个角色可以有多项权限,用户通过角色间接获得权限。在做表结构前,先想清楚“权限”在这里表示什么。
后台系统里最常见的权限分两类:菜单权限和操作权限。菜单权限决定用户登录后左侧导航能看到哪些页面;操作权限决定页面上哪些按钮能点、哪些接口能调。这两类在数据库里不应该分开建两套混乱的表,而是统一放在一张权限表里,通过 type 字段区分,再用父节点 id 维护树形层级。菜单权限的父节点是上一级目录,按钮权限的父节点是它所属的菜单。
2.2 五张表结构设计与 SQL
标准 RBAC 落地需要五张表:用户表、角色表、权限表、用户角色关联表、角色权限关联表。用户表和权限表之间不直接关联,也不要去建“用户权限关联表”,那样又退回给每个用户单独授权的模式了。下面是精简过的核心结构。
sql复制-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
status TINYINT DEFAULT 1 COMMENT '1正常 0停用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL,
role_code VARCHAR(50) NOT NULL UNIQUE,
status TINYINT DEFAULT 1,
remark VARCHAR(255)
);
-- 权限表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT DEFAULT 0 COMMENT '父节点id,菜单权限的上级是目录',
perm_name VARCHAR(50) NOT NULL COMMENT '菜单或按钮名称',
perm_code VARCHAR(100) NOT NULL COMMENT '权限标识,如 admin:user:add',
perm_type TINYINT NOT NULL COMMENT '1目录 2菜单 3按钮/接口',
sort INT DEFAULT 0,
status TINYINT DEFAULT 1
);
-- 用户角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
-- 角色权限关联表
CREATE TABLE sys_role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
索引方面,两张关联表我都用了联合主键,查询
