很多人学 Sa-Token第一篇文章看完后会觉得“登录我会了校验权限我也会了SaCheckPermission、SaCheckRole这些我都能写了。但是数据库怎么设计用户、角色、权限之间到底是什么关系菜单权限和按钮权限要不要分开管理员和普通用户怎么处理这些才是真正难的地方。”这篇文章就专门讲这个问题。如果你只是想写个 demo随便建三张表也能跑。但如果你想做一个能长期扩展的后台系统、SaaS 平台、管理端项目那权限表设计必须从一开始就想清楚。否则后期你会遇到这些问题角色越来越多关系越来越乱权限字符串到处写没人知道归谁管菜单和按钮混在一起前端不好渲染一个用户多个角色时权限计算容易出错新增业务模块后原有权限模型不够用以后想做部门隔离、租户隔离根本接不上所以这篇文章我不只讲“表怎么建”还会讲为什么要这样建每张表的职责是什么哪些字段必须有Sa-Token 该怎么接这些表实际项目里推荐怎么查角色和权限一、先说结论权限系统最常见的模型是什么大多数后台管理系统最稳定的一套设计其实就是用户表 角色表 权限表 用户角色中间表 角色权限中间表也就是经典的 RBAC 模型。RBAC 你可以简单理解成一句话不给用户直接绑一堆权限而是先给用户分配角色再由角色去拥有权限。比如张三是“管理员”李四是“财务”王五是“运营”然后管理员角色拥有用户管理、订单管理、系统设置等权限财务角色拥有退款审核、发票管理等权限运营角色拥有广告管理、内容配置等权限这样设计的最大好处是扩展性强。因为现实里权限不是按“人”一条条配的而是按“岗位”或“职责”来配的。你真正管理的是角色不是用户本身。二、为什么不建议“用户直接绑权限”很多新手最开始会这么想“我直接在用户表上搞个字段存这个用户有哪些权限不就完了”短期看很简单长期看会很痛苦。比如用户 A 需要 20 个权限用户 B 需要 18 个权限用户 C 需要 22 个权限你会发现权限配置越来越像“人工打补丁”。一旦用户数量多了完全不可维护。更麻烦的是两个用户如果岗位相同你还得重复配权限。后面某个功能权限改了你要一个个用户去改。所以实际项目里除非是非常简单的小系统否则不建议直接做“用户-权限”直连模型。更推荐的是用户 - 角色 - 权限这样才清晰。三、最基础的五张表应该怎么设计先给你完整结构sys_user用户表sys_role角色表sys_permission权限表sys_user_role用户角色关系表sys_role_permission角色权限关系表这五张表已经够支撑绝大多数管理系统。下面一张一张讲。四、用户表怎么设计用户表存的是“人”的基本信息。注意用户表的职责不是管权限它只负责记录用户本身。1. 推荐表结构CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 登录密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 用户昵称, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除0否 1是, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) COMMENT用户表;2. 这些字段各自有什么用id主键系统里唯一标识一个用户。Sa-Token 登录时最推荐用它StpUtil.login(user.getId());不要用用户名作为登录 ID。因为用户名可能改而主键 ID 一般不改。username登录账号。比如adminzhangsanfinance01这个字段用于登录校验。password登录密码。注意生产环境不能明文存储必须加密。一般建议用 BCrypt。nickname昵称。这个字段主要给前端展示不参与鉴权。real_name真实姓名。后台系统里很常用。比如审批流、日志追踪时更适合显示真实姓名。phone/email联系方式。后期可能用于短信登录、邮箱通知、找回密码。status用户状态。这个字段特别重要。建议至少有两种状态1正常0禁用登录时就可以判断if (user.getStatus() ! 1) { throw new RuntimeException(账号已被禁用); }is_deleted逻辑删除标记。很多业务系统不建议直接物理删除用户因为可能有关联日志、订单、审批记录。所以逻辑删除更稳妥。create_time/update_time这两个字段几乎是所有表的标配。后期查问题、审计数据、做列表页都用得上。五、角色表怎么设计角色表存的是“职责集合”。你可以把角色理解成一组权限的打包结果。比如超级管理员普通管理员财务运营审核员1. 推荐表结构CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, role_code VARCHAR(50) NOT NULL UNIQUE COMMENT 角色编码, role_name VARCHAR(50) NOT NULL COMMENT 角色名称, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) COMMENT角色表;2. 角色表字段说明role_code角色编码给后端做鉴权最合适。例如adminfinanceoperatorauditor对应 Sa-Token 的角色校验SaCheckRole(admin)也就是说SaCheckRole里写的字符串通常就对应数据库里的role_code。role_name角色名称给前端展示。例如超级管理员财务人员运营专员status角色是否启用。有时候某个角色废弃了但又不能直接删数据就可以禁用。remark备注。比如写一下这个角色是干什么的。六、权限表怎么设计权限表是整套系统里最关键的一张表。也是最容易设计乱的一张表。因为很多人一开始没有想清楚权限到底是接口权限还是菜单权限还是按钮权限答案是都可以但你必须统一设计规则。1. 推荐做法权限表统一管理“菜单、页面、按钮、接口能力”很多成熟系统会在一张权限表里统一维护资源只是用一个字段区分类型。这样前后端都会更方便。推荐结构如下CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, permission_code VARCHAR(100) NOT NULL UNIQUE COMMENT 权限编码, permission_name VARCHAR(100) NOT NULL COMMENT 权限名称, permission_type TINYINT NOT NULL COMMENT 类型1目录 2菜单 3按钮 4接口, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父级ID, path VARCHAR(200) DEFAULT NULL COMMENT 前端路由路径, component VARCHAR(200) DEFAULT NULL COMMENT 前端组件路径, api_url VARCHAR(200) DEFAULT NULL COMMENT 接口地址, sort INT NOT NULL DEFAULT 0 COMMENT 排序值, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, remark VARCHAR(255) DEFAULT NULL COMMENT 备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间 ) COMMENT权限表;2. 权限编码怎么命名这一点特别重要。你一旦命名混乱后面就会全乱。推荐统一成这种风格user:listuser:adduser:updateuser:deleteorder:listorder:refundinvoice:downloadadvertisement:publish这种命名的好处是一眼能看懂和业务模块对应和 Sa-Token 注解非常匹配比如SaCheckPermission(user:add)对应数据库里就是permission_code user:add3.permission_type为什么一定要有因为菜单和按钮不是一回事。比如后台用户管理模块用户管理菜单新增用户按钮编辑用户按钮删除用户按钮导出用户按钮这些都可以放在权限表里但必须区分类型。建议约定1目录2菜单3按钮4接口这样后面你就能前端根据目录和菜单构建侧边栏页面根据按钮权限控制按钮显示隐藏后端根据接口权限控制访问4.parent_id有什么用这个字段用来做树形结构。比如系统管理用户管理用户查询用户新增用户删除角色管理角色新增角色编辑这样前端就可以很方便构建菜单树。5.path和component为什么有时也要放如果你做的是前后端分离后台系统很多项目会把菜单信息直接存在后端数据库里然后登录后把菜单树返回给前端。前端据此动态渲染路由。这时path前端访问路径component页面组件路径就很有用。如果你系统没做动态路由这两个字段也可以先留空。6.api_url要不要存可以存但不是强制。它的价值主要是做资源台账后期审计更清楚方便管理某个权限对应的接口但注意不要指望只靠api_url自动做权限控制。大多数项目里真正稳定的做法仍然是接口上写权限编码数据库里也存权限编码。也就是代码里SaCheckPermission(user:add)数据库里也有user:add这比拿 URL 去匹配更稳。七、用户角色关系表怎么设计一个用户通常不止一个角色。比如某人既是“运营”又兼“审核员”。所以用户和角色一般是多对多关系。这时就需要中间表。1. 推荐表结构CREATE TABLE sys_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, user_id BIGINT NOT NULL COMMENT 用户ID, role_id BIGINT NOT NULL COMMENT 角色ID, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) COMMENT用户角色关系表;2. 为什么不建议把role_id直接放用户表里因为那样只能支持一个角色。现实项目里一个用户多角色非常常见。所以中间表是更合理的设计。3. 这一层在代码里怎么查比如你想查用户有哪些角色思路就是先从sys_user_role查出role_id再去sys_role查出对应role_code最终返回给 Sa-TokenOverride public ListString getRoleList(Object loginId, String loginType) { Long userId Long.valueOf(String.valueOf(loginId)); return roleService.findRoleCodesByUserId(userId); }八、角色权限关系表怎么设计角色和权限同样是多对多关系。一个角色会拥有很多权限一个权限也可能被多个角色拥有。所以也需要中间表。1. 推荐表结构CREATE TABLE sys_role_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, role_id BIGINT NOT NULL COMMENT 角色ID, permission_id BIGINT NOT NULL COMMENT 权限ID, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) COMMENT角色权限关系表;2. 这一层的意义是什么它负责描述哪个角色拥有哪些权限比如admin拥有user:add、user:delete、role:updatefinance拥有invoice:list、invoice:downloadoperator拥有advertisement:list、advertisement:publish后面用户拥有角色角色再映射权限整条链路就通了。九、五张表之间的关系怎么理解最清楚你可以直接记住这条链用户 - 用户角色关系表 - 角色 - 角色权限关系表 - 权限也就是说先知道当前是谁查他有哪些角色再根据角色查出所有权限最后把权限编码列表返回给 Sa-Token这就是最标准、最清晰的一套逻辑。十、Sa-Token 在这套表结构里怎么接下面讲最关键的落地部分。你有了数据库表最终还是要让 Sa-Token 能用。核心就是实现StpInterface。1. 角色查询实现Component public class StpInterfaceImpl implements StpInterface { Autowired private RoleService roleService; Autowired private PermissionService permissionService; Override public ListString getPermissionList(Object loginId, String loginType) { Long userId Long.valueOf(String.valueOf(loginId)); return permissionService.findPermissionCodesByUserId(userId); } Override public ListString getRoleList(Object loginId, String loginType) { Long userId Long.valueOf(String.valueOf(loginId)); return roleService.findRoleCodesByUserId(userId); } }2.findRoleCodesByUserId的查询思路SQL 逻辑通常是这样SELECT r.role_code FROM sys_role r JOIN sys_user_role ur ON ur.role_id r.id WHERE ur.user_id #{userId} AND r.status 1;这里要注意只查启用状态的角色如果你有逻辑删除字段也要过滤掉3.findPermissionCodesByUserId的查询思路最常见的写法SELECT DISTINCT p.permission_code FROM sys_permission p JOIN sys_role_permission rp ON rp.permission_id p.id JOIN sys_user_role ur ON ur.role_id rp.role_id JOIN sys_role r ON r.id ur.role_id WHERE ur.user_id #{userId} AND p.status 1 AND r.status 1;这里为什么用DISTINCT因为用户可能有多个角色而多个角色又可能拥有同一个权限。不去重的话权限列表会重复。十一、接口鉴权时数据库设计和代码怎么配合有了上面的表结构接口权限控制就很直观了。比如你有一个新增用户接口SaCheckPermission(user:add) PostMapping(/user/add) public String addUser() { return success; }当用户访问这个接口时Sa-Token 会根据当前 token 找到loginId调用getPermissionList(loginId, loginType)拿到该用户全部权限编码判断里面是否包含user:add如果有放行。如果没有拦截。所以你会发现代码里的权限字符串必须和数据库里的permission_code对应起来。这就是为什么权限编码命名一定要规范。十二、菜单权限和按钮权限到底要不要分建议分而且要在同一张权限表里通过字段区分。原因很简单菜单权限解决的是“能不能看见页面入口”比如左侧菜单“用户管理”是否显示。按钮权限解决的是“进了页面后能不能操作”比如“新增用户”按钮是否显示。这两者都属于权限但粒度不同。推荐做法后端登录后返回当前用户的菜单树前端根据菜单树渲染左侧导航同时返回按钮权限编码集合前端页面里根据权限编码控制按钮显示这样结构最清晰。十三、一个后台系统的权限数据可以怎么初始化为了让你更直观我给你举一套例子。1. 角色数据INSERT INTO sys_role (role_code, role_name, status, remark) VALUES (admin, 超级管理员, 1, 拥有系统全部权限), (finance, 财务, 1, 负责退款、发票等相关操作), (operator, 运营, 1, 负责广告、内容、活动配置);2. 权限数据INSERT INTO sys_permission (permission_code, permission_name, permission_type, parent_id, sort, status) VALUES (user:list, 用户列表, 3, 0, 1, 1), (user:add, 新增用户, 3, 0, 2, 1), (user:update, 编辑用户, 3, 0, 3, 1), (user:delete, 删除用户, 3, 0, 4, 1), (invoice:list, 发票列表, 3, 0, 5, 1), (invoice:download, 下载发票, 3, 0, 6, 1), (advertisement:list, 广告列表, 3, 0, 7, 1), (advertisement:publish, 发布广告, 3, 0, 8, 1);3. 角色权限关系例如admin拥有全部finance拥有发票相关operator拥有广告相关这样后面分配用户就简单多了。十四、超级管理员要不要单独处理这个问题很常见。建议可以单独处理但不要滥用。比如你可以约定admin角色默认拥有全部权限或者在代码里加一个超管判断public ListString findPermissionCodesByUserId(Long userId) { if (isSuperAdmin(userId)) { return permissionRepository.findAllPermissionCodes(); } return permissionRepository.findPermissionCodesByUserId(userId); }这么做的好处是超级管理员不用手动分配所有权限新增权限后超管自动拥有但注意一点只有真正的超级管理员才这么处理。不要为了省事让大量管理员都走“全部权限”逻辑否则权限模型就失控了。十五、是否需要“用户直接附加权限”有些项目里用户除了角色继承权限外还会有少量个性化权限。比如某个员工临时多一个导出权限。这时有两种做法第一种严格只走角色优点是模型简单。缺点是不够灵活。第二种角色权限 用户直连权限也就是再加一张表sys_user_permission这种设计更灵活但复杂度也会上来。如果你的项目目前还不复杂我建议先别上这一层。先把RBAC 标准模型做稳。后面真的有需要再扩展。十六、部门、租户、数据范围要不要一开始就设计进去这个要看你的项目体量。如果你现在做的是普通后台系统单公司内部系统单租户 SaaS那先不用把模型搞得太重。先把用户、角色、权限这五张表做好就够了。但如果你明确后面会做多租户平台按公司隔离数据按部门控制数据范围一个账号属于不同组织那后面可能还要扩展租户表部门表用户部门关系角色数据范围字段不过这些是下一阶段的事。别在第一版就把系统设计得太复杂。十七、实际项目里推荐怎么封装服务层不建议把所有 SQL 都塞进一个类里。更推荐拆成两个核心查询1. 角色服务public interface RoleService { ListString findRoleCodesByUserId(Long userId); }2. 权限服务public interface PermissionService { ListString findPermissionCodesByUserId(Long userId); }这样StpInterfaceImpl就只负责对接 Sa-Token不负责业务细节。例如Component public class StpInterfaceImpl implements StpInterface { Autowired private RoleService roleService; Autowired private PermissionService permissionService; Override public ListString getPermissionList(Object loginId, String loginType) { Long userId Long.valueOf(String.valueOf(loginId)); return permissionService.findPermissionCodesByUserId(userId); } Override public ListString getRoleList(Object loginId, String loginType) { Long userId Long.valueOf(String.valueOf(loginId)); return roleService.findRoleCodesByUserId(userId); } }这种职责划分会清楚很多。十八、哪些字段是后期大概率会补的很多人第一版表结构做得太“纯”后面又疯狂加字段。我提前给你几个常见扩展点。用户表可能补充avatar头像last_login_time最后登录时间last_login_ip最后登录IPdept_id部门IDtenant_id租户ID角色表可能补充data_scope数据范围tenant_id租户ID权限表可能补充icon菜单图标visible是否显示cacheable页面是否缓存这些不是第一版必须有但你心里最好有数。十九、最容易犯的几个设计错误这一部分很适合写在文章里因为读者会很有共鸣。错误一把菜单、按钮、接口完全分成三套体系这样后面维护会非常痛苦。推荐统一在权限表中管理用类型区分。错误二权限编码乱命名比如有的叫addUsersysUserAddUSER_INSERTuser-add全项目一堆不同风格后面没人能维护。最好统一成模块:动作例如user:adduser:updateorder:refund错误三用户表直接存一个角色字段短期能用长期一定不够。错误四权限表不做状态字段后面权限废弃、暂停使用时会很麻烦。错误五角色和权限查询不去重多角色用户经常会查出重复权限。一定要DISTINCT。二十、最推荐的方案如果你现在正在做 Spring Boot Sa-Token 项目我建议你就按下面这套来。第一步建五张表sys_usersys_rolesys_permissionsys_user_rolesys_role_permission第二步权限编码统一命名成模块:动作第三步菜单、按钮、接口统一进权限表用permission_type区分第四步实现StpInterfacegetRoleList()查角色编码getPermissionList()查权限编码第五步接口上统一写注解SaCheckPermission(user:add) SaCheckRole(admin)第六步后期再逐步扩展超级管理员租户部门数据权限这样最稳不容易推倒重来。二十一、总结Sa-Token 的使用其实不难真正决定系统上限的往往不是 API而是你的权限模型。如果表设计混乱哪怕你SaCheckPermission写得再漂亮后面项目还是会越来越难维护。但如果一开始就把这几层关系理顺用户是谁角色是什么权限是什么用户和角色怎么关联角色和权限怎么关联那整套系统就会非常顺。你可以把整件事理解成一句话Sa-Token 负责“校验”而数据库表设计负责“供数”。校验能不能稳取决于你的供数模型是不是清楚