后台权限系统设计规范
版本:v0.2.4 | 最后更新:2026-03-12
变更记录:
- v0.1.0:初版设计(8 张表 DDL + 缓存 + 校验规则)
- v0.2.0:基于技术评审修订 6 项(防提权、缓存升级、禁用优先级、实现约束等)
- v0.2.1:基于二次评审补齐 4 项必须项 + 4 项建议项(评审人:e50u)
- 明确关联表硬删策略
- superadmin 唯一性保障(仅初始化,不提供创建接口)
- auth_version 批量递增策略(同步批量 SQL)
- JWT 立即失效机制(Redis session 绑定)
- 默认群组白名单改为只读动作校验
- 目录型菜单 perm_id 规则
- 资源可见性后端强制过滤
- 操作日志脱敏字段清单
- v0.2.2:三次评审最终定稿(评审人:e50u)
- auth_version 以 DB 为权威来源,session 不承担版本判定职责
- 删除权限前置引用检查(menu/api 引用时禁止删除)
- session 结构移除 auth_version 字段
- v0.2.3:终审修正(评审人:e50u)
- 修正 §5.1 JWT 模型描述,session 存储字段与 §5.3/§6.3 统一为
{user_id, login_time, ip}- v0.2.4:基于第二轮评审修订(评审人:e50u,P0 + P1 共 8 项)
- [P0] 新增 §5.4 会话续期策略(滑动窗口续期,明确 refresh 边界)
- [P0] §5.3 明确 v0.2.x 为单端登录硬约束,不支持多端并发
- [P1] sys_group / sys_permission / sys_menu / sys_api 新增
created_by字段,§8.2 可见性规则对齐- [P1] §3.5 新增软删后唯一标识复用策略
- [P1] §5.1 补充 JWT 最低 claim 规范
- [P1] 新增 §5.5 登录安全基线
- [P1] §9.3 补充 Redis session 删除失败补偿机制
- [P1] 新增附录 A:接口清单
1. 概述
后台权限系统采用 系统角色 + 群组授权 模型:
- 系统角色(superadmin / admin / user)控制系统管理权限,硬编码在 Middleware 中
- 群组承载业务权限,用户只能通过关联群组获得业务权限,不支持直接授权给用户
- 权限表(
sys_permission)作为单一事实来源,菜单和 API 均引用 permission_id
权限流转
用户 → 关联群组 → 群组拥有权限 → 权限关联菜单/API
系统角色层级
| 角色 | role_type | 说明 |
|---|---|---|
| 超级管理员 | superadmin | 唯一,Middleware 硬编码全放行,不走群组 |
| 管理员 | admin | 由 superadmin 添加,可管理群组和普通用户 |
| 普通用户 | user | 由 admin 添加,业务权限完全由群组决定 |
管理权限矩阵
| 操作 | superadmin | admin | user |
|---|---|---|---|
| 添加/管理 admin | ✅ | ❌ | ❌ |
| 添加/管理 user | ✅ | ✅ | ❌ |
| 创建/管理群组 | ✅ | ✅ | ❌ |
| 给群组分配权限 | ✅ | ✅(不超过自身) | ❌ |
| 修改自身权限 | ❌ | ❌ | ❌ |
| 修改自身基本信息 | ✅ | ✅ | ✅ |
设计约束
- superadmin 全局唯一,仅通过系统初始化创建,后台不提供创建 superadmin 的接口
- 用户业务权限 = 所有关联群组权限的并集
- admin 分配权限时,可分配范围 = 自身所有群组权限的并集
- 禁止修改自身权限,只允许修改基本信息
- 新增用户时支持复制指定用户的群组关联,但不能超过操作者自身权限
- admin 不能看到/操作超出自身权限范围的资源
禁用优先级
任一层禁用即不生效,采用"短路"策略:
用户 status=禁用 → 拒绝(不查后续)
→ 群组 status=禁用 → 该群组权限不计入用户权限集合
→ 权限 status=禁用 → 该权限不计入
→ API status=禁用 → 该 API 不可访问
→ 菜单 status=禁用 → 前端不渲染
- 禁用用户:立即失效,等同于踢出(删除 Redis session + 权限缓存)
- 禁用群组:该群组下所有用户失去该群组带来的权限
- 禁用权限:所有关联该权限的群组均受影响
- 禁用 API/菜单:对应资源不可访问/不渲染
admin 防提权规则
admin 存在通过管理自身所属群组间接提权的风险,需严格封堵:
| 场景 | 规则 |
|---|---|
| admin 修改自己所属群组的权限 | 禁止。admin 不能修改自己是成员的群组的权限配置 |
| admin 把自己加入其他群组 | 禁止。不允许修改自身的群组关联 |
| admin 创建新群组并加入 | 创建允许,但不能把自己加入(需 superadmin 或其他 admin 操作) |
| admin 修改群组权限后再加入 | 两步操作均需校验:修改权限时不能超出自身范围,加入群组不能自己操作 |
核心原则:admin 的权限变更路径上,不允许出现"自己给自己"的操作。
2. 表结构(8 张表)
2.1 sys_user — 用户表
CREATE TABLE `sys_user` (
`user_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名(登录用)',
`password` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '密码(bcrypt)',
`nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
`email` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '邮箱',
`phone` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '手机号',
`avatar` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '头像URL',
`role_type` VARCHAR(16) NOT NULL DEFAULT 'user' COMMENT '系统角色:superadmin/admin/user',
`auth_version` BIGINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '权限版本号(权限变更时递增)',
`status` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态:1=启用 2=禁用',
`last_login_at` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '最后登录时间(Unix时间戳)',
`last_login_ip` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '最后登录IP',
`is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除:0=正常 1=已删除',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`update_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`user_id`),
UNIQUE KEY `uniq_username` (`username`),
KEY `idx_role_type` (`role_type`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统用户表';
2.2 sys_group — 群组表
CREATE TABLE `sys_group` (
`group_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '群组ID',
`group_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '群组名称',
`group_key` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '群组标识(唯一)',
`description` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '群组描述',
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建人用户ID',
`is_default` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否默认群组:0=否 1=是',
`status` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态:1=启用 2=禁用',
`sort_order` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '排序权重',
`is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除:0=正常 1=已删除',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`update_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`group_id`),
UNIQUE KEY `uniq_group_key` (`group_key`),
KEY `idx_is_default` (`is_default`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统群组表';
2.3 sys_user_group — 用户群组关联表
CREATE TABLE `sys_user_group` (
`ug_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '关联ID',
`user_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '用户ID',
`group_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '群组ID',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`ug_id`),
UNIQUE KEY `uniq_user_group` (`user_id`, `group_id`),
KEY `idx_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户群组关联表';
2.4 sys_permission — 权限定义表
CREATE TABLE `sys_permission` (
`perm_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`permission_key` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '权限标识:模块:资源:动作',
`permission_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '权限名称',
`module` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '所属模块',
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建人用户ID',
`description` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '权限描述',
`status` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态:1=启用 2=禁用',
`sort_order` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '排序权重',
`is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除:0=正常 1=已删除',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`update_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`perm_id`),
UNIQUE KEY `uniq_permission_key` (`permission_key`),
KEY `idx_module` (`module`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='权限定义表';
2.5 sys_group_permission — 群组权限关联表
CREATE TABLE `sys_group_permission` (
`gp_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '关联ID',
`group_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '群组ID',
`perm_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '权限ID',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`gp_id`),
UNIQUE KEY `uniq_group_perm` (`group_id`, `perm_id`),
KEY `idx_perm_id` (`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='群组权限关联表';
2.6 sys_menu — 菜单表
CREATE TABLE `sys_menu` (
`menu_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '父菜单ID(0=顶级)',
`perm_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '关联权限ID(目录型可为0)',
`menu_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '菜单名称',
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建人用户ID',
`menu_type` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '类型:1=目录 2=菜单 3=按钮',
`path` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '路由路径',
`component` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '前端组件路径',
`icon` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '图标',
`is_visible` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '是否可见:0=隐藏 1=显示',
`is_cache` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否缓存:0=否 1=是',
`sort_order` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '排序权重',
`status` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态:1=启用 2=禁用',
`is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除:0=正常 1=已删除',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`update_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`menu_id`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_perm_id` (`perm_id`),
KEY `idx_sort_order` (`sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统菜单表';
2.7 sys_api — API 资源表
CREATE TABLE `sys_api` (
`api_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'API ID',
`perm_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '关联权限ID',
`api_path` VARCHAR(256) NOT NULL DEFAULT '' COMMENT 'API路径',
`api_method` VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'HTTP方法:GET/POST/PUT/DELETE',
`api_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'API名称',
`api_group` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'API分组',
`created_by` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建人用户ID',
`description` VARCHAR(256) NOT NULL DEFAULT '' COMMENT 'API描述',
`status` TINYINT UNSIGNED NOT NULL DEFAULT '1' COMMENT '状态:1=启用 2=禁用',
`is_deleted` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除:0=正常 1=已删除',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`update_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '更新时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`api_id`),
UNIQUE KEY `uniq_path_method` (`api_path`, `api_method`),
KEY `idx_perm_id` (`perm_id`),
KEY `idx_api_group` (`api_group`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统API资源表';
2.8 sys_operation_log — 操作日志表
CREATE TABLE `sys_operation_log` (
`log_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`trace_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '链路追踪ID',
`operator_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '操作人用户ID',
`operator_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作人用户名',
`target_type` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '操作对象类型:user/group/permission/menu/api',
`target_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '操作对象ID',
`action` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '动作:create/update/delete/grant/revoke/login/logout',
`module` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '所属模块',
`description` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '操作描述',
`before_snapshot` JSON COMMENT '变更前快照',
`after_snapshot` JSON COMMENT '变更后快照',
`ip` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '操作IP',
`user_agent` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'User-Agent',
`request_method` VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'HTTP方法',
`request_path` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '请求路径',
`request_body` TEXT COMMENT '请求体(已脱敏,见§10.2)',
`response_code` INT NOT NULL DEFAULT '0' COMMENT '响应状态码',
`duration_ms` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '耗时(毫秒)',
`create_time` BIGINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '创建时间(Unix时间戳)',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`log_id`),
KEY `idx_trace_id` (`trace_id`),
KEY `idx_operator_id` (`operator_id`),
KEY `idx_target` (`target_type`, `target_id`),
KEY `idx_action` (`action`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统操作日志表';
3. 删除与停用策略
3.1 删除策略
主表软删,关联表硬删。
| 表类型 | 删除方式 | 说明 |
|---|---|---|
| 主表(sys_user / sys_group / sys_permission / sys_menu / sys_api) | 软删除(is_deleted=1) | 保留业务主记录,支持审计追溯 |
| 关联表(sys_user_group / sys_group_permission) | 物理删除 | 关联关系是派生数据,不承担业务主记录语义;审计依赖 sys_operation_log |
| 日志表(sys_operation_log) | 不删除 | 只增不删,按时间归档 |
3.2 删除级联行为
| 操作 | 级联行为 |
|---|---|
| 删除用户 | 软删用户 + 物理删除 user_group 关联 + 删除 Redis session + 递增 auth_version |
| 删除群组 | 软删群组 + 物理删除 user_group + group_permission 关联 + 递增受影响用户 auth_version |
| 删除权限 | 前置检查:若仍被 menu 或 api 引用,拒绝删除(需先解绑)。通过后:软删权限 + 物理删除 group_permission 关联 + 递增受影响用户 auth_version |
| 删除菜单 | 软删菜单,子菜单不级联(需手动处理或提示"存在子菜单,请先处理") |
| 删除 API | 软删 API |
3.3 停用规则
停用不删除关联关系,仅在运行时过滤:
| 操作 | 行为 |
|---|---|
| 停用用户 | 立即踢出(删除 Redis session + 权限缓存),后续登录拒绝 |
| 停用群组 | 该群组权限不计入任何用户的权限集合,递增受影响用户的 auth_version |
| 停用权限 | 该权限不计入任何群组的权限集合,递增受影响用户的 auth_version |
| 停用 API | 该 API 对所有用户返回 403 |
| 停用菜单 | 前端不渲染,不影响后端 API 访问 |
3.5 软删后唯一标识复用策略
软删除(is_deleted=1)后,原唯一标识(username、group_key、permission_key)仍占用唯一索引,可能阻碍新记录使用相同标识。
策略:软删时追加后缀,释放唯一标识。
| 表 | 唯一字段 | 软删处理 |
|---|---|---|
| sys_user | username | 更新为 {原值}__del_{user_id} |
| sys_group | group_key | 更新为 {原值}__del_{group_id} |
| sys_permission | permission_key | 更新为 {原值}__del_{perm_id} |
- 追加
__del_{id}后缀确保唯一性且可追溯原始值 - 软删操作在同一事务内完成:
UPDATE ... SET is_deleted=1, username=CONCAT(username, '__del_', user_id) - 查询时
WHERE is_deleted=0过滤已删除记录,唯一索引保留全量覆盖 - 恢复(如有需要)时去掉
__del_后缀,需校验原标识是否已被新记录占用
3.4 默认群组安全边界
- 默认群组(
is_default=1)不得包含高风险权限 - 默认群组仅允许包含只读动作权限(permission_key 的动作部分必须为
list/view/read/detail) - 给默认群组分配权限时,接口需校验 permission_key 的动作后缀是否在只读白名单内
- 默认群组不可被删除或停用
4. 初始化数据
-- 初始化 superadmin(密码需实际部署时 bcrypt 加密)
-- ⚠️ superadmin 仅通过此初始化脚本创建,后台不提供创建 superadmin 的接口
INSERT INTO `sys_user` (`user_id`, `username`, `password`, `nickname`, `role_type`, `status`, `create_time`, `update_time`)
VALUES (1, 'superadmin', '$2a$10$PLACEHOLDER', '超级管理员', 'superadmin', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 初始化默认群组
INSERT INTO `sys_group` (`group_id`, `group_name`, `group_key`, `description`, `is_default`, `status`, `create_time`, `update_time`)
VALUES (1, '默认群组', 'default', '新用户自动加入的默认群组(仅包含只读权限)', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
5. JWT 与会话管理
5.1 JWT 模型
采用 access_token + Redis session 模型:
| 项目 | 说明 |
|---|---|
| access_token | JWT 签发,有效期 2h |
| Redis session | Key: sys:session:{session_id},存储 user_id、login_time、ip,TTL 24h(滑动续期) |
JWT 最低 Claim 规范
access_token 的 JWT payload 必须且仅包含以下字段:
{
"sub": "user_id(字符串化的用户ID)",
"sid": "session_id(UUID v4)",
"role": "role_type(superadmin/admin/user)",
"iat": 1741776000,
"exp": 1741783200
}
| Claim | 类型 | 必填 | 说明 |
|---|---|---|---|
sub | string | ✅ | 用户 ID(标准 JWT subject) |
sid | string | ✅ | 会话 ID,用于关联 Redis session |
role | string | ✅ | 系统角色,用于 Middleware 层快速判断 |
iat | number | ✅ | 签发时间(Unix 时间戳) |
exp | number | ✅ | 过期时间(签发后 2h) |
禁止在 JWT payload 中存放权限列表、邮箱、手机号等敏感或易变数据。
5.2 Token 立即失效机制
JWT 每次请求时必须校验 Redis session 是否存在:
请求进入
→ 从 JWT 解析 session_id
→ 查 Redis sys:session:{session_id}
→ 不存在 → 401(token 已失效)
→ 存在 → 继续鉴权
立即失效触发:
- 停用用户 → 删除该用户的 Redis session
- 删除用户 → 删除该用户的 Redis session
- 修改密码 → 删除该用户的 Redis session(需重新登录)
- 新设备登录 → 删除旧 session,旧端自动踢出
5.3 Session Key 设计
| Key | 类型 | 值 | TTL |
|---|---|---|---|
sys:session:{session_id} | Hash | {user_id, login_time, ip} | 24h |
sys:user:{uid}:session | String | 当前活跃的 session_id | 24h |
v0.2.x 硬约束:单端登录,禁止多端并发。 同一时间同一用户只允许一个 Session 活跃。此为当前版本的设计约束,多端并发登录作为 v0.3+ 候选项评估。
- 用户登录时,检查
sys:user:{uid}:session是否已存在旧 session_id- 若存在 → 删除旧
sys:session:{旧session_id}→ 旧端自动踢出
- 若存在 → 删除旧
- 写入新 session_id 到
sys:user:{uid}:session - 旧端下次请求时,session 已不存在 → 返回 401(已在其他设备登录)
5.4 会话续期策略
采用 滑动窗口续期,无 refresh_token:
| 项目 | 策略 | 说明 |
|---|---|---|
| access_token(JWT) | 不续期,到期重新登录 | JWT 签发后有效期固定 2h,过期后需重新登录获取新 token |
| Redis session | 滑动续期 | 每次有效请求命中 session 时,重置 TTL 为 24h |
续期流程:
请求进入
→ 校验 JWT(exp 未过期)
→ 校验 Redis session 存在
→ 存在 → EXPIRE sys:session:{session_id} 86400(续期 24h)
→ 不存在 → 401
→ 继续鉴权
设计说明:
- 不引入 refresh_token:v0.2.x 为后台管理系统,用户活跃期间 Redis session 持续续期,access_token 2h 过期后重新登录即可。refresh_token 增加攻击面且当前场景收益有限。
- access_token 不续期的原因:JWT 一旦签发不可撤销(除非走 Redis session 校验),缩短有效期 + 不续期可降低 token 泄漏风险。
- Redis session 续期条件:仅在 JWT 校验通过 + session 存在时续期,避免无效请求刷新 TTL。
- 用户体验:只要用户在 24h 内有活跃请求,session 就不会过期;超过 24h 无活动则需重新登录。
5.5 登录安全基线
| 项目 | 规则 | 说明 |
|---|---|---|
| 密码强度 | 最少 8 位,必须包含大小写字母 + 数字 | 不强制特殊字符,降低用户负担 |
| 密码存储 | bcrypt,cost ≥ 10 | 禁止 MD5/SHA 系列 |
| 登录失败锁定 | 连续 5 次失败 → 锁定 15 分钟 | Redis 计数器 sys:login:fail:{username},TTL 15min |
| 锁定期间行为 | 返回 "账号已锁定,请 N 分钟后重试" | 不区分"用户不存在"和"密码错误",防枚举 |
| 登录日志 | 所有登录尝试(成功 + 失败)均写入 sys_operation_log | action=login / login_fail |
| 密码修改 | 修改密码后立即删除所有 Redis session,强制重新登录 | 包含当前 session |
| 首次登录 | 不强制修改密码(v0.2.x 暂不实现) | v0.3 候选项 |
登录失败计数流程:
登录请求
→ 检查 sys:login:fail:{username}
→ 次数 ≥ 5 → 直接拒绝(不查 DB)
→ 查 DB 校验密码
→ 失败 → INCR sys:login:fail:{username},首次设 TTL 15min
→ 成功 → DEL sys:login:fail:{username},创建 session
6. Redis 缓存设计
6.1 版本号 + 懒失效模型
采用版本号机制解决缓存一致性问题:
核心思路:
sys_user.auth_version为权威版本号(DB 维护)- Redis 缓存中同时存储
version字段 - 每次鉴权时比对版本号,版本落后则重建缓存
- 权限变更只需递增受影响用户的
auth_version,无需主动删缓存
6.2 缓存结构
| Key | 类型 | 值 | TTL |
|---|---|---|---|
sys:user:{uid}:auth | Hash | {version, role_type, permissions, status} | 30min |
合并为单个 Hash,避免 role_type 和 permissions 分开缓存导致读时撕裂。
6.3 版本比对权威来源
Session 只负责会话有效性,DB sys_user.auth_version 为权限版本唯一权威来源。
- Redis session 中不存储 auth_version,只存
{user_id, login_time, ip} - 鉴权时 cache version 与 DB auth_version 比对
- DB auth_version 本身可缓存到独立 Redis key(
sys:user:{uid}:auth_version,TTL 30s),避免每次查 DB
6.4 鉴权流程
请求进入
→ 从 JWT 取 user_id + session_id
→ 查 Redis sys:session:{session_id}
→ 不存在 → 401
→ 查 Redis sys:user:{uid}:auth(Hash)
→ 缓存存在
→ 获取 DB auth_version(优先从 Redis 短缓存读,miss 则查 DB)
→ 比对 cache.version 与 DB auth_version
→ 版本一致 → 使用缓存
→ 版本落后 → 重建缓存
→ 缓存不存在
→ singleflight 防击穿,查 DB 重建缓存
→ 检查 status
→ status=禁用 → 拒绝 + 删除 session
→ 检查 role_type
→ superadmin → 放行(仍需校验 status 和 session 有效性)
→ 当前 API 的 permission_key ∈ permissions 集合?
→ 是 → 放行
→ 否 → 403
6.5 版本号递增策略
| 触发事件 | 操作 | SQL 策略 |
|---|---|---|
| 用户加入/退出群组 | 递增该用户的 auth_version | 单条 UPDATE |
| 群组权限变更 | 递增该群组下所有用户的 auth_version | 批量 SQL:UPDATE sys_user SET auth_version = auth_version + 1 WHERE user_id IN (SELECT user_id FROM sys_user_group WHERE group_id = ?) |
| 权限定义变更/停用 | 递增所有关联该权限的群组下的所有用户的 auth_version | 批量 SQL:通过 group_permission → user_group 链路查出 user_id 集合,一条 UPDATE |
| 群组停用/删除 | 递增该群组下所有用户的 auth_version | 同群组权限变更 |
| 用户 role_type 变更 | 递增该用户的 auth_version | 单条 UPDATE |
| 用户停用 | 递增 auth_version + 删除所有 Redis session | 单条 UPDATE + Redis DEL |
实现要求:
- 必须使用 set-based SQL,禁止 for-loop 单条更新
- auth_version 递增在同一事务内完成(与关联关系变更同事务)
- 事务失败则全部回滚,缓存短 TTL(30min)兜底
6.6 防击穿
缓存重建使用 singleflight(Go golang.org/x/sync/singleflight),同一用户并发请求只触发一次 DB 查询。
6.7 降级策略
Redis 不可用时,直接查 DB 鉴权,不缓存,记告警日志。
7. 权限映射示例
7.1 permission → API 映射(1:N)
一个权限可以关联多个 API:
permission: system:user:list
→ GET /api/v1/users (用户列表)
→ GET /api/v1/users/:id (用户详情)
permission: system:user:create
→ POST /api/v1/users (创建用户)
permission: system:user:update
→ PUT /api/v1/users/:id (更新用户)
permission: system:user:delete
→ DELETE /api/v1/users/:id (删除用户)
7.2 菜单权限绑定规则
| 菜单类型 | perm_id 要求 |
|---|---|
| 目录(menu_type=1) | 允许 perm_id=0(目录是容器,不一定对应权限) |
| 菜单(menu_type=2) | 必须绑定 perm_id(对应页面访问权限) |
| 按钮(menu_type=3) | 必须绑定 perm_id(对应操作权限) |
示例:
目录:系统管理(menu_type=1, perm_id=0)
├── 菜单:用户管理(menu_type=2, perm_id → system:user:list)
│ ├── 按钮:新增(menu_type=3, perm_id → system:user:create)
│ ├── 按钮:编辑(menu_type=3, perm_id → system:user:update)
│ └── 按钮:删除(menu_type=3, perm_id → system:user:delete)
└── 菜单:群组管理(menu_type=2, perm_id → system:group:list)
├── 按钮:新增(menu_type=3, perm_id → system:group:create)
└── 按钮:分配权限(menu_type=3, perm_id → system:group:grant)
前端根据用户权限集合决定渲染哪些目录、菜单和按钮。目录下如果所有子菜单都无权限,则目录也不渲染。
8. 接口校验规则
8.1 权限校验
| 规则 | 说明 |
|---|---|
| 权限边界 | admin 分配权限时,只能从自身权限并集中选取 |
| 角色层级 | 不允许操作比自己角色高的用户(admin 不能改 superadmin) |
| 自身限制 | 不允许修改自身的 role_type、群组关联 |
| 群组自身限制 | admin 不能修改自己所属群组的权限配置(防间接提权) |
| 复制权限 | 新增用户时复制目标用户的群组,逐条校验不超过操作者权限 |
| 批量操作 | 逐条校验,一条失败全部回滚 |
| 默认群组保护 | 默认群组不可删除/停用,只能分配只读动作权限 |
| superadmin 保护 | 不提供创建 superadmin 接口;不允许通过接口修改 superadmin 的 role_type |
8.2 资源可见性过滤
后端强制过滤,不依赖前端。
| 接口 | 过滤规则 |
|---|---|
| 群组列表 | admin 只能看到自己所属的群组 + 自己创建的群组 |
| 权限树 | admin 只能看到自身权限范围内的权限项 |
| 用户可分配群组列表 | 只返回操作者有权限管理的群组 |
| 可分配权限列表 | 只返回操作者自身权限并集内的权限 |
所有列表/搜索/详情接口,都必须在 Service 层按操作者权限边界裁剪后再返回,不能返回全量数据让前端过滤。
9. 实现约束
9.1 分层职责
按 backend-spec 分层,权限模块代码组织:
internal/
controller/system/ # API 入口,参数校验,调用 service
service/system/ # 业务逻辑,权限校验,事务管理
dao/system/ # 数据访问,SQL 查询
model/system/ # 表结构体 + DTO
middleware/ # auth.go(JWT + session)+ permission.go(鉴权)
- Controller 只做参数绑定和响应,不写业务逻辑
- Service 负责权限校验、事务管理、缓存操作
- DAO 只做数据读写,不做业务判断
9.2 错误码规划
权限模块错误码统一前缀 1xxx:
| 错误码 | 含义 | HTTP |
|---|---|---|
| 1001 | 未登录 / Token 无效 | 401 |
| 1002 | Token 已过期 | 401 |
| 1003 | 权限不足 | 403 |
| 1004 | 用户已禁用 | 403 |
| 1005 | 越权操作(操作超出自身权限范围) | 403 |
| 1006 | 角色越级(admin 不能操作 superadmin) | 403 |
| 1007 | 禁止修改自身权限 | 403 |
| 1008 | 禁止修改自身所属群组权限 | 403 |
| 1009 | superadmin 已存在,不可重复创建 | 400 |
| 1010 | 默认群组不可删除/停用 | 400 |
| 1011 | 用户名已存在 | 400 |
| 1012 | 群组标识已存在 | 400 |
| 1013 | 权限标识已存在 | 400 |
| 1014 | 复制权限超出操作者范围 | 403 |
| 1015 | 会话已失效,请重新登录 | 401 |
| 1016 | 默认群组仅允许只读权限 | 400 |
| 1017 | 权限仍被菜单或API引用,请先解绑 | 400 |
错误码必须定义在 internal/pkg/defined/errors.go,Controller 层使用 ResFail(ctx, err) 返回。
9.3 事务边界
以下操作必须在事务内完成:
| 操作 | 事务范围 |
|---|---|
| 删除用户 | 软删用户 + 物理删除 user_group 关联 + 递增 auth_version |
| 删除群组 | 软删群组 + 物理删除 user_group + group_permission 关联 + 递增受影响用户 auth_version |
| 删除权限 | 前置检查 menu/api 引用 → 软删权限 + 物理删除 group_permission 关联 + 递增受影响用户 auth_version |
| 批量分配群组权限 | 全量替换 group_permission + 递增受影响用户 auth_version |
| 复制用户群组 | 创建用户 + 批量创建 user_group |
| 停用用户 | 更新 status + 递增 auth_version(事务后删除 Redis session) |
事务使用 RunNestedTx,遵循 backend-spec 嵌套事务规范。Redis 操作(删除 session)在事务提交后执行。
Redis Session 删除失败补偿机制
DB 事务提交后,Redis session 删除可能失败(网络抖动、Redis 不可用),需补偿:
| 步骤 | 说明 |
|---|---|
| 1. 即时重试 | 删除失败后立即重试 1 次(同步) |
| 2. 异步补偿 | 仍失败则写入补偿队列(内存 channel),后台 goroutine 定时重试(间隔 5s,最多 3 次) |
| 3. 兜底过期 | Redis session 本身有 TTL(24h),即使删除彻底失败,最迟 24h 后自动过期 |
| 4. 告警 | 异步补偿仍失败 → 记 ERROR 日志 + 告警(便于运维介入) |
关键原则:DB 事务不因 Redis 失败回滚。 Redis 操作是最终一致性,TTL 是终极兜底。
9.4 Trace 链路
- 所有 API 请求携带
trace_id(由 Middleware 生成或从 HeaderX-Trace-Id提取) trace_id贯穿 Controller → Service → DAO → 操作日志- 使用
logging.Context(ctx)记录日志,不使用zap.L() - 操作日志表的
trace_id字段用于关联请求链路
9.5 测试要求
| 层 | 测试类型 | 覆盖重点 |
|---|---|---|
| DAO | 单元测试 | CRUD 正确性 |
| Service | 单元测试 + 集成测试 | 权限校验逻辑、防提权、边界条件 |
| Middleware | 集成测试 | 鉴权流程、superadmin 放行、禁用用户拦截、session 失效 |
| API | 端到端测试 | 完整请求链路 |
核心验收用例(必须通过):
- admin 尝试给自己提权 → 拒绝(1007)
- admin 尝试修改自己所属群组的权限 → 拒绝(1008)
- admin 尝试通过创建群组→分配权限→加入群组间接提权 → 加入步骤拒绝(1007)
- 权限被回收后,旧缓存不可继续访问(auth_version 递增,缓存失效)
- 群组禁用后,用户权限即时失效
- 复制用户群组时,越权复制被拦截(1014)
- 停用用户后,已有 token 立即失效(Redis session 已删除)
- superadmin 被禁用后也无法登录(status 优先于 role_type)
- 默认群组分配非只读权限 → 拒绝(1016)
- 后台创建 superadmin → 拒绝(1009)
10. 操作日志规范
10.1 记录范围
所有用户可感知的操作必须记录,包括但不限于:
| 模块 | 记录动作 |
|---|---|
| 用户管理 | create / update / delete / 停用 / 启用 / 修改密码 / 修改角色 |
| 群组管理 | create / update / delete / 停用 / 启用 |
| 权限管理 | create / update / delete / 停用 / 启用 |
| 群组权限 | grant(分配权限)/ revoke(回收权限) |
| 用户群组 | grant(加入群组)/ revoke(移出群组) |
| 菜单管理 | create / update / delete |
| API 管理 | create / update / delete |
| 登录/登出 | login / logout |
10.2 脱敏字段清单
操作日志中 request_body 和 before_snapshot / after_snapshot 必须对以下字段脱敏:
| 字段 | 脱敏规则 | 示例 |
|---|---|---|
password | 完全替换 | "******" |
token / access_token / refresh_token | 完全替换 | "******" |
phone | 中间 4 位替换 | 138****5678 |
email | @ 前保留首尾各 1 位 | f***6@gmail.com |
id_card | 中间 10 位替换 | 3101**********1234 |
脱敏在 Service 层写入日志前执行,DAO 层不感知。
11. Roadmap
v0.3 候选项(不阻塞当前版本开工)
| 事项 | 说明 |
|---|---|
| 数据权限(channel_id 隔离) | 当前 Out of Scope。后续如有多组织/多业务线需求,需在 Middleware 层增加数据范围过滤。风险:若不做,所有 admin 可看到全部业务数据 |
| deny 模型(显式拒绝) | 当前仅 allow-list。后续可增加黑名单机制,优先级:deny > allow |
| 权限版本审计增强 | 记录 auth_version 每次变更的原因和触发源 |
| 告警联动 | 高风险操作(新增 admin、修改 admin 群组、批量授权、变更默认群组)接入告警通知 |
| 多端登录 | v0.2.x 硬约束单端;后续可扩展为设备管理 + 多 session 并发 |
| 首次登录强制改密 | v0.2.x 不实现,v0.3 评估 |
附录 A:接口清单
以下为权限模块核心接口,供前后端联调参考。所有接口前缀
/api/v1,请求/响应遵循 backend-spec 统一格式。
A.1 认证
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| POST | /auth/login | 登录(返回 access_token) | 公开 |
| POST | /auth/logout | 登出(删除 session) | 已登录 |
| GET | /auth/me | 获取当前用户信息 + 权限列表 | 已登录 |
A.2 用户管理
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /users | 用户列表(分页) | system:user:list |
| GET | /users/:id | 用户详情 | system:user:list |
| POST | /users | 创建用户 | system:user:create |
| PUT | /users/:id | 更新用户 | system:user:update |
| DELETE | /users/:id | 删除用户(软删) | system:user:delete |
| PUT | /users/:id/status | 启用/停用用户 | system:user:update |
| PUT | /users/:id/password | 重置用户密码 | system:user:update |
| PUT | /users/me/password | 修改自己的密码 | 已登录 |
| PUT | /users/me/profile | 修改自己的基本信息 | 已登录 |
A.3 群组管理
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /groups | 群组列表 | system:group:list |
| GET | /groups/:id | 群组详情 | system:group:list |
| POST | /groups | 创建群组 | system:group:create |
| PUT | /groups/:id | 更新群组 | system:group:update |
| DELETE | /groups/:id | 删除群组(软删) | system:group:delete |
| PUT | /groups/:id/status | 启用/停用群组 | system:group:update |
| GET | /groups/:id/permissions | 查看群组权限 | system:group:list |
| PUT | /groups/:id/permissions | 分配群组权限(全量替换) | system:group:grant |
| GET | /groups/:id/users | 查看群组成员 | system:group:list |
| POST | /groups/:id/users | 添加群组成员 | system:group:grant |
| DELETE | /groups/:id/users/:uid | 移除群组成员 | system:group:grant |
A.4 权限管理
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /permissions | 权限列表(支持按 module 过滤) | system:permission:list |
| GET | /permissions/:id | 权限详情 | system:permission:list |
| POST | /permissions | 创建权限 | system:permission:create |
| PUT | /permissions/:id | 更新权限 | system:permission:update |
| DELETE | /permissions/:id | 删除权限(前置引用检查) | system:permission:delete |
A.5 菜单管理
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /menus | 菜单树 | system:menu:list |
| POST | /menus | 创建菜单 | system:menu:create |
| PUT | /menus/:id | 更新菜单 | system:menu:update |
| DELETE | /menus/:id | 删除菜单 | system:menu:delete |
A.6 API 管理
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /apis | API 列表(支持按 group 过滤) | system:api:list |
| POST | /apis | 创建 API | system:api:create |
| PUT | /apis/:id | 更新 API | system:api:update |
| DELETE | /apis/:id | 删除 API | system:api:delete |
A.7 操作日志
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /operation-logs | 操作日志列表(分页,支持按操作人/对象/动作过滤) | system:log:list |