Skip to main content

后台权限系统设计规范

版本: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 添加,业务权限完全由群组决定

管理权限矩阵

操作superadminadminuser
添加/管理 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)后,原唯一标识(usernamegroup_keypermission_key)仍占用唯一索引,可能阻碍新记录使用相同标识。

策略:软删时追加后缀,释放唯一标识。

唯一字段软删处理
sys_userusername更新为 {原值}__del_{user_id}
sys_groupgroup_key更新为 {原值}__del_{group_id}
sys_permissionpermission_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_tokenJWT 签发,有效期 2h
Redis sessionKey: sys:session:{session_id},存储 user_idlogin_timeip,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类型必填说明
substring用户 ID(标准 JWT subject)
sidstring会话 ID,用于关联 Redis session
rolestring系统角色,用于 Middleware 层快速判断
iatnumber签发时间(Unix 时间戳)
expnumber过期时间(签发后 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}:sessionString当前活跃的 session_id24h

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_logaction=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}:authHash{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批量 SQLUPDATE 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
1002Token 已过期401
1003权限不足403
1004用户已禁用403
1005越权操作(操作超出自身权限范围)403
1006角色越级(admin 不能操作 superadmin)403
1007禁止修改自身权限403
1008禁止修改自身所属群组权限403
1009superadmin 已存在,不可重复创建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 生成或从 Header X-Trace-Id 提取)
  • trace_id 贯穿 Controller → Service → DAO → 操作日志
  • 使用 logging.Context(ctx) 记录日志,不使用 zap.L()
  • 操作日志表的 trace_id 字段用于关联请求链路

9.5 测试要求

测试类型覆盖重点
DAO单元测试CRUD 正确性
Service单元测试 + 集成测试权限校验逻辑、防提权、边界条件
Middleware集成测试鉴权流程、superadmin 放行、禁用用户拦截、session 失效
API端到端测试完整请求链路

核心验收用例(必须通过):

  1. admin 尝试给自己提权 → 拒绝(1007)
  2. admin 尝试修改自己所属群组的权限 → 拒绝(1008)
  3. admin 尝试通过创建群组→分配权限→加入群组间接提权 → 加入步骤拒绝(1007)
  4. 权限被回收后,旧缓存不可继续访问(auth_version 递增,缓存失效)
  5. 群组禁用后,用户权限即时失效
  6. 复制用户群组时,越权复制被拦截(1014)
  7. 停用用户后,已有 token 立即失效(Redis session 已删除)
  8. superadmin 被禁用后也无法登录(status 优先于 role_type)
  9. 默认群组分配非只读权限 → 拒绝(1016)
  10. 后台创建 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_bodybefore_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/apisAPI 列表(支持按 group 过滤)system:api:list
POST/apis创建 APIsystem:api:create
PUT/apis/:id更新 APIsystem:api:update
DELETE/apis/:id删除 APIsystem:api:delete

A.7 操作日志

方法路径说明权限
GET/operation-logs操作日志列表(分页,支持按操作人/对象/动作过滤)system:log:list