|
|
1 day ago | |
|---|---|---|
| cli | 4 days ago | |
| etc | 3 days ago | |
| internal | 1 day ago | |
| pb | 4 weeks ago | |
| permclient | 4 weeks ago | |
| .gitignore | 4 days ago | |
| .gitmodules | 1 month ago | |
| README.md | 1 day ago | |
| audit-report.md | 3 weeks ago | |
| gen-api.sh | 1 month ago | |
| gen-model.sh | 5 days ago | |
| go.mod | 4 days ago | |
| go.sum | 4 days ago | |
| perm.api | 1 day ago | |
| perm.go | 4 weeks ago | |
| perm.sql | 4 weeks ago | |
| run-test.sh | 1 month ago | |
| test-design.md | 1 day ago | |
| test-report.md | 1 day ago |
集中式多产品权限管理平台后端服务。为多个产品提供统一的用户认证、权限管理、角色管理能力,产品通过 HTTP API 或 gRPC 接入。
多产品隔离 — 权限、角色、成员按产品(productCode)隔离,一个账号可跨产品通用
灵活的权限模型 — 角色权限 + 用户级 ALLOW/DENY 覆盖,细粒度控制
研发部门自动授权 — 研发部门(deptType=DEV)的成员加入产品后自动拥有全部权限
双协议 — 同时提供 HTTP REST API(管理 UI)和 gRPC(产品后端高性能调用)
自动权限同步 — 产品启动时通过 API 自动上报权限列表,系统自动新增/更新/禁用
JWT 本地验证 — 登录获取 JWT,产品后端可本地验证,无需每次请求回调权限系统
登录端隔离 — 产品端(/auth/login)和管理后台(/auth/adminLogin)独立登录接口,超管仅能通过管理后台登录
令牌轮转 — refreshToken 刷新时自动递增 tokenVersion,旧令牌即时失效,防止令牌被盗后的无限续期
主动注销 — /auth/logout 接口递增 tokenVersion 使所有已签发令牌立即失效
产品级熔断 — 禁用产品后所有在线成员的令牌即时失效,HTTP/gRPC 双通道同步拦截
最后管理员保护 — 移除或降级产品最后一个 ADMIN 时自动拒绝,防止产品进入无管理状态
graph TB
subgraph 权限系统
HTTP[HTTP API :10001]
GRPC[gRPC Server :10002]
DB[(MySQL)]
CACHE[(Redis Cache)]
HTTP --> DB
HTTP --> CACHE
GRPC --> DB
GRPC --> CACHE
end
UI[管理后台 UI] -->|REST API| HTTP
ProductA[产品 A 后端] -->|gRPC| GRPC
ProductB[产品 B 后端] -->|gRPC| GRPC
ProductC[产品 C 后端] -->|HTTP| HTTP
erDiagram
sys_product ||--o{ sys_perm : "productCode → 产品拥有权限"
sys_product ||--o{ sys_role : "productCode → 产品拥有角色"
sys_product ||--o{ sys_product_member : "productCode → 产品拥有成员"
sys_user ||--o{ sys_product_member : "userId → 用户加入产品"
sys_user ||--o{ sys_user_role : "userId → 用户绑定角色"
sys_user ||--o{ sys_user_perm : "userId → 用户自定义权限"
sys_user }o--o| sys_dept : "deptId → 用户所属部门"
sys_dept }o--o| sys_dept : "parentId → 父子部门"
sys_role ||--o{ sys_role_perm : "roleId → 角色绑定权限"
sys_role ||--o{ sys_user_role : "roleId → 角色分配给用户"
sys_perm ||--o{ sys_role_perm : "permId → 权限绑定到角色"
sys_perm ||--o{ sys_user_perm : "permId → 权限直接授予用户"
sys_product {
bigint id PK
varchar code UK "产品编码"
varchar name "产品名称"
varchar appKey UK "接入密钥"
varchar appSecret "签名密钥"
tinyint status "1启用 2禁用"
}
sys_user {
bigint id PK
varchar username UK "登录名"
varchar password "bcrypt密码"
varchar nickname "昵称"
bigint deptId FK "部门ID"
tinyint isSuperAdmin "1是 2否"
tinyint mustChangePassword "1是 2否"
tinyint status "1正常 2冻结"
}
sys_dept {
bigint id PK
bigint parentId "父部门ID"
varchar name "部门名称"
varchar path "层级路径"
varchar deptType "NORMAL普通 DEV研发"
int sort "排序值"
tinyint status "1启用 2禁用"
}
sys_perm {
bigint id PK
varchar productCode "所属产品"
varchar name "权限名"
varchar code UK "权限code"
tinyint status "1启用 2禁用"
}
sys_role {
bigint id PK
varchar productCode "所属产品"
varchar name UK "角色名"
int permsLevel "权限等级"
tinyint status "1启用 2禁用"
}
sys_product_member {
bigint id PK
varchar productCode "产品编码"
bigint userId FK "用户ID"
varchar memberType "成员类型"
tinyint status "1启用 2禁用"
}
sys_role_perm {
bigint id PK
bigint roleId FK
bigint permId FK
}
sys_user_role {
bigint id PK
bigint userId FK
bigint roleId FK
}
sys_user_perm {
bigint id PK
bigint userId FK
bigint permId FK
varchar effect "ALLOW或DENY"
}
| 实体 | 说明 | 隔离范围 |
|---|---|---|
| 产品 (Product) | 接入权限系统的业务系统(如 CRM、OA),每个产品有独立的 appKey/appSecret |
全局 |
| 部门 (Dept) | 组织架构树形结构,支持无限层级嵌套。deptType=DEV 的研发部门有特殊权限逻辑 |
全局 |
| 用户 (User) | 全局账号,通过 deptId 归属部门,通过成员关系加入不同产品 |
全局 |
| 角色 (Role) | 产品级别的权限集合,同一角色名在不同产品中互不干扰 | 产品级 |
| 权限 (Perm) | 产品级别的最小权限单元,由产品后端通过 SyncPerms 自动上报 |
产品级 |
全局组织架构 产品权限体系(按产品隔离)
┌─────────────────┐ ┌────────────────────────────────────┐
│ 部门 (Dept) │ │ 产品 (Product) │
│ ├── 研发部(DEV) │ │ ┌─────────┐ ┌──────────┐ │
│ ├── 市场部 │ │ │ 权限列表 │ │ 角色列表 │ │
│ └── 财务部 │ │ │ Perm │ │ Role │ │
└───────┬─────────┘ │ └────┬────┘ └────┬─────┘ │
│ deptId │ │ │ │
┌───────┴──────┐ │ ┌────┴────────────┴────┐ │
│ 用户 (User) │─────────────│ │ 角色-权限绑定 RolePerm │ │
│ │ 成员关系 │ └─────────────────────┘ │
│ │──────────────│ │
│ │ │ ┌───────────────────────┐ │
│ │──────────────│ │ 用户-角色 UserRole │ │
│ │ │ └───────────────────────┘ │
│ │──────────────│ ┌───────────────────────┐ │
│ │ │ │ 用户-权限覆盖 UserPerm │ │
└──────────────┘ │ │ ALLOW / DENY │ │
│ └───────────────────────┘ │
└────────────────────────────────────┘
sys_product_member 建立,一个用户可以是多个产品的成员sys_user.deptId 关联,一个用户只属于一个部门sys_role_perm 绑定,一个角色可绑定多个权限sys_user_role 分配,一个用户可拥有多个角色sys_user_perm 直接授予/拒绝,优先级高于角色权限| 优先级 | 类型 | 权限范围 | 来源 |
|---|---|---|---|
| 1 | SUPER_ADMIN |
所有产品的全部权限 | sys_user.isSuperAdmin = 1 |
| 2 | DEVELOPER |
该产品全部权限 | sys_product_member.memberType |
| 3 | ADMIN |
该产品全部权限 | sys_product_member.memberType |
| 4 | 研发部门成员 | 该产品全部权限 | sys_dept.deptType = DEV + 是产品成员 |
| 5 | MEMBER |
角色权限 ∪ ALLOW - DENY | 角色绑定 + 用户级覆盖计算 |
flowchart TD
START[获取用户权限] --> CHK_PROD{产品已启用?}
CHK_PROD -->|否| EMPTY_PROD[返回空权限]
CHK_PROD -->|是| IS_SUPER{是超级管理员?}
IS_SUPER -->|是| ALL_PERMS[返回该产品全部权限码]
IS_SUPER -->|否| FIND_MEMBER[查询 sys_product_member]
FIND_MEMBER --> HAS_MEMBER{是活跃成员?}
HAS_MEMBER -->|否| EMPTY[返回空权限]
HAS_MEMBER -->|是| CHECK_TYPE{成员类型?}
CHECK_TYPE -->|DEVELOPER / ADMIN| ALL_PERMS
CHECK_TYPE -->|MEMBER| CHECK_DEPT{所属部门是研发部门?}
CHECK_DEPT -->|是| ALL_PERMS
CHECK_DEPT -->|否| CALC[计算权限]
CALC --> ROLE_PERMS[查询用户角色绑定的权限 ID]
ROLE_PERMS --> USER_ALLOW[查询用户级 ALLOW 权限 ID]
USER_ALLOW --> USER_DENY[查询用户级 DENY 权限 ID]
USER_DENY --> MERGE["合并: (角色权限 ∪ ALLOW) - DENY"]
MERGE --> FILTER[过滤已禁用的权限]
FILTER --> CODES[转换为权限 code 列表返回]
以下以一个典型的中小公司为例,说明如何配置和使用权限系统。
公司有研发部(30 人)、市场部(10 人)、运营部(15 人),负责维护 3 个产品:CRM 系统、OA 系统、电商后台。研发人员需要在产品间流动。
graph TD
ROOT[公司] --> DEV[研发部 deptType=DEV]
ROOT --> MKT[市场部 deptType=NORMAL]
ROOT --> OPS[运营部 deptType=NORMAL]
DEV --> FE[前端组]
DEV --> BE[后端组]
DEV --> QA[测试组]
# 创建顶级部门
POST /api/dept/create {"parentId": 0, "name": "研发部", "deptType": "DEV", "sort": 1}
# 返回 id=1
POST /api/dept/create {"parentId": 0, "name": "市场部", "sort": 2}
# deptType 不传默认为 NORMAL,返回 id=2
POST /api/dept/create {"parentId": 0, "name": "运营部", "sort": 3}
# 返回 id=3
# 创建研发部子部门
POST /api/dept/create {"parentId": 1, "name": "前端组", "deptType": "DEV", "sort": 1}
POST /api/dept/create {"parentId": 1, "name": "后端组", "deptType": "DEV", "sort": 2}
POST /api/dept/create {"parentId": 1, "name": "测试组", "deptType": "DEV", "sort": 3}
研发部门的特殊能力:
deptType=DEV的部门中的成员,只要被添加到某个产品下,就自动拥有该产品的全部权限。当从产品成员中移除时,权限立即收回。
POST /api/product/create {"code": "crm", "name": "CRM 系统"}
# 返回 appKey, appSecret, adminUser, adminPassword
POST /api/product/create {"code": "oa", "name": "OA 系统"}
POST /api/product/create {"code": "mall", "name": "电商后台"}
每个产品创建后会自动生成一个初始管理员账号(如 admin_crm),该账号是该产品的 ADMIN 级别成员。
各产品后端在启动时自动调用 POST /api/perm/sync 上报权限列表。以 CRM 为例:
{
"appKey": "...", "appSecret": "...",
"perms": [
{"code": "customer:list", "name": "查看客户列表"},
{"code": "customer:create", "name": "创建客户"},
{"code": "customer:update", "name": "编辑客户"},
{"code": "customer:delete", "name": "删除客户"},
{"code": "order:list", "name": "查看订单"},
{"code": "order:create", "name": "创建订单"},
{"code": "report:export", "name": "导出报表"}
]
}
权限由产品代码自动管理,管理员无需手动创建。
# CRM 产品下创建角色
POST /api/role/create {"productCode": "crm", "name": "销售经理", "permsLevel": 10}
POST /api/role/create {"productCode": "crm", "name": "普通销售", "permsLevel": 20}
POST /api/role/create {"productCode": "crm", "name": "客服", "permsLevel": 30}
# 为角色绑定权限
POST /api/role/bindPerms {"roleId": 1, "permIds": [1,2,3,4,5,6,7]} # 销售经理:全部
POST /api/role/bindPerms {"roleId": 2, "permIds": [1,2,5,6]} # 普通销售:查看+创建
POST /api/role/bindPerms {"roleId": 3, "permIds": [1,5]} # 客服:仅查看
场景 A:研发人员张三 — 属于研发部,需要参与 CRM 和 OA 的开发
# 创建用户,归属研发部
POST /api/user/create {"username": "zhangsan", "nickname": "张三", "deptId": 1}
# 返回 userId=10, credentialsTicket(用于领取初始密码)
# 将张三添加为 CRM 和 OA 的成员(MEMBER 类型即可)
POST /api/member/add {"productCode": "crm", "userId": 10, "memberType": "MEMBER"}
POST /api/member/add {"productCode": "oa", "userId": 10, "memberType": "MEMBER"}
# 因为张三属于研发部(DEV),他登录 CRM 或 OA 时自动拥有全部权限
# 无需为他分配角色!
场景 B:一段时间后张三不再负责 OA — 只需移除成员关系
POST /api/member/remove {"id": <张三在OA中的成员记录ID>}
# OA 的权限立即收回,CRM 的权限不受影响
场景 C:市场部李四 — 需要使用 CRM,分配销售角色
POST /api/user/create {"username": "lisi", "nickname": "李四", "deptId": 2}
# 返回 userId=11, credentialsTicket
POST /api/member/add {"productCode": "crm", "userId": 11, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 11, "roleIds": [2], "productCode": "crm"} # 分配"普通销售"角色
李四登录 CRM 后拥有的权限 = 普通销售角色的权限 = [customer:list, customer:create, order:list, order:create]。
场景 D:给李四额外权限 — 在角色基础上微调
# 额外给李四"导出报表"权限,但禁止他"创建订单"
POST /api/user/setPerms {
"userId": 11,
"perms": [
{"permId": 7, "effect": "ALLOW"},
{"permId": 6, "effect": "DENY"}
]
}
最终李四的权限 = 角色 [1,2,5,6] ∪ ALLOW [7] - DENY [6] = [customer:list, customer:create, order:list, report:export]
场景 E:外部临时协作人员 — 不属于任何部门
POST /api/user/create {"username": "temp_wang", "nickname": "外包王五"}
# deptId 不传,不归属任何部门
POST /api/member/add {"productCode": "crm", "userId": 12, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 12, "roleIds": [3], "productCode": "crm"} # 分配"客服"角色(只读)
flowchart TD
Q1{该用户是研发人员?}
Q1 -->|是| A1[归属研发部门 deptType=DEV]
A1 --> A2["添加为产品成员(MEMBER)即可<br>自动拥有全部权限"]
Q1 -->|否| Q2{该用户是产品管理员?}
Q2 -->|是| A3["添加为产品成员(ADMIN)<br>自动拥有全部权限"]
Q2 -->|否| Q3{需要精细权限控制?}
Q3 -->|是| A4["添加为产品成员(MEMBER)"]
A4 --> A5[分配角色 + 可选 ALLOW/DENY 覆盖]
Q3 -->|否| A6[不添加为成员 = 无权限]
系统内置一套集中式操作权限管控机制,对所有管理类接口实施多维度的访问控制,防止越权操作。
flowchart LR
REQ[HTTP 请求] --> L1["① JWT 中间件<br>解析 token / 验证签名"]
L1 --> L2["② UserDetailsLoader<br>加载用户实时状态"]
L2 --> L3["③ 多维状态检查<br>用户/产品/成员状态"]
L3 --> L4["④ Logic 层<br>操作权限控制"]
L4 --> BIZ[业务逻辑]
| 层级 | 组件 | 职责 |
|---|---|---|
| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 tokenType、校验 tokenVersion |
| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限、产品状态) |
| 第三层 | 多维状态检查 | 用户冻结(status ≠ 1)→ 403;产品禁用(productStatus ≠ 1)→ 403;成员无效(memberType 为空)→ 403 |
| 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 |
| 接口 | 权限要求 | 额外检查 |
|---|---|---|
| 产品管理 | ||
| 创建产品 | 仅超级管理员 | — |
| 更新产品 | 仅超级管理员 | — |
| 部门管理 | ||
| 创建部门 | 仅超级管理员 | — |
| 更新部门 | 仅超级管理员 | — |
| 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
| 角色管理 | ||
| 创建角色 | 超管 或 产品管理员 | 产品必须启用 |
| 更新角色 | 超管 或 产品管理员 | 非超管不可降低 permsLevel |
| 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
| 绑定角色权限 | 超管 或 产品管理员 | — |
| 用户管理 | ||
| 创建用户 | 超管 或 产品管理员 | 服务端生成密码,ticket 下发 |
| 重置密码 | 通过 CheckManageAccess |
不可重置超管密码 |
| 获取用户凭证 | 超管 或 产品管理员 | ticket 一次性消费 |
| 更新用户信息 | 仅本人 或 超管 | — |
| 冻结/解冻用户 | 通过 CheckManageAccess |
不可冻结自己和超管 |
| 绑定角色 | 通过 CheckManageAccess |
目标用户的成员状态必须启用 |
| 设置权限覆盖 | 通过 CheckManageAccess |
目标用户的成员状态必须启用;产品必须启用 |
| 查看权限覆盖 | 本人自查无需额外检查;查他人需通过 CheckManageAccess |
userId 枚举防护:非超管非本人时先检查产品管理员身份 |
| 成员管理 | ||
| 添加成员 | 通过 CheckManageAccess + CheckMemberTypeAssignment |
不可分配同级或更高类型;产品必须启用 |
| 更新成员 | 通过 CheckManageAccess + CheckMemberTypeAssignment |
不可降级最后一个 ADMIN |
| 移除成员 | 通过 CheckManageAccess |
不可移除最后一个 ADMIN;级联清理角色/权限绑定 |
| 查询类接口 | ||
| 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
| 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
| 认证接口 | ||
| 修改自身信息 (updateInfo) | 已登录即可 | 仅允许修改昵称/头像/邮箱/手机,userId 从 JWT 获取 |
| 用户注销 (logout) | 已登录即可 | 递增 tokenVersion,所有已签发令牌即时失效 |
| 公开接口 | ||
| 产品端登录 (login) | 无需鉴权 | cap.js 已启用时拒绝(必须走 login/cap);未启用时需携带图片验证码;超级管理员被拒绝,productCode 必传 |
| 产品端 cap.js 登录 (auth/login/cap) | 无需鉴权 | cap.js 未启用时拒绝(必须走 login);提交 cap token 完成人机验证后登录 |
| 管理后台登录 (adminLogin) | 无需鉴权 | cap.js 已启用时拒绝(必须走 adminLogin/cap);未启用时需携带图片验证码;需验证 managementKey |
| 管理后台 cap.js 登录 (adminLogin/cap) | 无需鉴权 | cap.js 未启用时拒绝(必须走 adminLogin);提交 cap token 完成人机验证后登录 |
| 获取图片验证码 (captcha/get) | 无需鉴权 | cap.js 未启用时前端调用此接口展示图片验证码 |
| 获取 cap.js 端点 (capjs/endpoint) | 无需鉴权 | 返回 cap.js 端点 URL;空串代表 cap.js 未启用,前端降级为图片验证码 |
| 刷新令牌 | 无需鉴权 | 令牌轮转:旧令牌即时失效,返回全新令牌对;产品禁用时拒绝 |
| 同步权限 | 无需鉴权 | 通过 appKey/appSecret 认证 |
CheckManageAccess 是管理类操作的核心权限检查函数,执行多维度判定:
flowchart TD
START[CheckManageAccess] --> SA{是超级管理员?}
SA -->|是| PASS[✅ 通过]
SA -->|否| SELF{操作自己?}
SELF -->|是| PASS
SELF -->|否| DEPT[部门层级检查]
DEPT --> IS_ADMIN{操作者是 ADMIN?}
IS_ADMIN -->|是| LEVEL[权限级别检查]
IS_ADMIN -->|否| HAS_DEPT{操作者有部门?}
HAS_DEPT -->|是| TARGET_DEPT{目标在操作者<br>本部门或子部门?}
HAS_DEPT -->|否| DENY1[❌ 未归属部门]
TARGET_DEPT -->|是| LEVEL
TARGET_DEPT -->|否| DENY2[❌ 跨部门拒绝]
LEVEL --> CMP_TYPE{比较 memberType 优先级}
CMP_TYPE -->|操作者更高| PASS
CMP_TYPE -->|目标更高| DENY3[❌ 权限不足]
CMP_TYPE -->|同级| CMP_PERM{比较 permsLevel}
CMP_PERM -->|操作者更小| PASS
CMP_PERM -->|相等或更大| DENY4[❌ 同级或更低]
SUPER_ADMIN 不受任何限制ADMIN 和超管豁免此检查)memberType 优先级,同级再比 permsLevel 数值
memberType 优先级:SUPER_ADMIN(0) > ADMIN(1) > DEVELOPER(2) > MEMBER(3)permsLevel:数值越小权限越高(如 10 > 20),无角色默认为 MaxInt64(最低)UserDetailsLoader 是一个集中式的用户信息加载与缓存组件,为中间件、登录、用户信息查询等多个场景提供统一的数据来源。
| 数据来源 | 加载的字段 |
|---|---|
sys_user |
userId, username, nickname, avatar, email, phone, remark, isSuperAdmin, mustChangePassword, status |
sys_dept |
deptId, deptName, deptPath, deptType |
sys_product |
productCode, productName, productStatus |
sys_product_member |
memberType |
sys_role (当前产品) |
roles[], minPermsLevel |
| 计算后的权限 | perms[] (权限 code 集合) |
{prefix}:ud:{userId}:{productCode}| 操作 | 失效方法 | 失效范围 |
|---|---|---|
| 更新用户信息 / 冻结解冻 / 修改密码 / 注销 | Clean(userId) |
该用户所有产品缓存 |
| 设置用户权限覆盖 / 添加成员 / 更新成员 | Del(userId, productCode) |
该用户在指定产品的缓存 |
| 更新角色 / 删除角色 / 绑定角色权限 | BatchDel(userIds, productCode) |
受影响用户在指定产品的缓存 |
| 更新产品 / 同步权限 | CleanByProduct(productCode) |
该产品下所有用户的缓存 |
| 更新部门 | Clean(uid) × N |
该部门下所有用户的缓存 |
| 刷新令牌 | Clean(userId) |
轮转时递增 tokenVersion 后清除缓存 |
# 1. 创建数据库并导入表结构
mysql -u root -p -e "CREATE DATABASE perms_system DEFAULT CHARACTER SET utf8mb4;"
mysql -u root -p perms_system < perm.sql
# 2. 修改配置(数据库、Redis、JWT 密钥等)
vim etc/perm-api.yaml
# 3. 安装依赖并启动
go mod tidy
go run perm.go
# 服务启动后:
# - HTTP API: http://localhost:10001
# - gRPC: localhost:10002
首次部署后,需手动在数据库中插入超级管理员账号:
INSERT INTO sys_user (username, password, nickname, isSuperAdmin, status, createTime, updateTime)
VALUES ('superadmin', '$2a$10$这里替换为bcrypt加密后的密码', '超级管理员', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
可使用任意 bcrypt 工具生成密码哈希,或在 Go 中执行:
hash, _ := bcrypt.GenerateFromPassword([]byte("your-password"), bcrypt.DefaultCost)
fmt.Println(string(hash))
本节以 "CRM 系统"(编码 crm)为例,完整演示接入流程。
sequenceDiagram
participant SA as 超级管理员
participant PS as 权限系统
participant CRM as CRM 后端
participant U as 终端用户
rect rgb(230, 245, 255)
Note over SA, PS: 阶段一:注册产品
SA ->> PS: POST /api/product/create
PS -->> SA: appKey + appSecret + 初始管理员
end
rect rgb(230, 255, 230)
Note over CRM, PS: 阶段二:CRM 启动 → 同步权限
CRM ->> PS: POST /api/perm/sync (appKey + appSecret + 权限列表)
PS -->> CRM: {added, updated, disabled}
end
rect rgb(255, 245, 230)
Note over SA, PS: 阶段三:管理员配置角色与用户
SA ->> PS: 创建角色、绑定权限、创建用户、分配角色
end
rect rgb(245, 230, 255)
Note over U, CRM: 阶段四:用户登录与鉴权
U ->> CRM: 提交用户名密码
CRM ->> PS: POST /api/auth/login 或 gRPC Login
PS -->> CRM: accessToken + refreshToken + perms[]
CRM -->> U: 返回 token
U ->> CRM: 业务请求 + Bearer token
CRM ->> CRM: 本地 JWT 解析,检查 perms[]
CRM -->> U: 返回业务数据
end
超级管理员登录后创建产品:
POST /api/product/create
{"code": "crm", "name": "CRM 系统", "remark": "客户关系管理", "adminDeptId": 1}
响应中包含 credentialsTicket(一次性凭证票据,5 分钟内有效),请立即用该 ticket 调用 /api/product/fetchInitialCredentials 领取 appKey、appSecret、adminUser、adminPassword。ticket 一次性消费,过期或已使用后无法重新获取。
CRM 后端在启动阶段上报全部权限列表。此接口无需 JWT,通过 appKey + appSecret 认证。
POST /api/perm/sync
{
"appKey": "a1b2c3d4e5f6...",
"appSecret": "x9y8z7w6v5u4...",
"perms": [
{"code": "customer:list", "name": "查看客户列表"},
{"code": "customer:create", "name": "创建客户"},
{"code": "customer:update", "name": "编辑客户"},
{"code": "customer:delete", "name": "删除客户"},
{"code": "order:list", "name": "查看订单列表"},
{"code": "order:create", "name": "创建订单"},
{"code": "report:export", "name": "导出报表"}
]
}
var permsList = []map[string]string{
{"code": "customer:list", "name": "查看客户列表"},
{"code": "customer:create", "name": "创建客户"},
// ...
}
func syncPermsOnStartup(permSystemURL, appKey, appSecret string) {
body, _ := json.Marshal(map[string]interface{}{
"appKey": appKey, "appSecret": appSecret, "perms": permsList,
})
resp, err := http.Post(permSystemURL+"/api/perm/sync", "application/json", bytes.NewReader(body))
if err != nil {
log.Fatalf("同步权限失败: %v", err)
}
defer resp.Body.Close()
log.Println("权限同步完成")
}
详见上文「实际业务配置指南」。
POST /api/auth/login
{"username": "zhangsan", "password": "123456", "productCode": "crm"}
import "perms-system-server/permclient"
client, err := permclient.NewPermClient("perm-system:10002")
resp, err := client.Login(ctx, "crm", "zhangsan", "123456")
// resp.AccessToken, resp.RefreshToken, resp.Perms
产品后端配置与权限系统相同的 AccessSecret,本地解析 JWT 即可获取用户信息和权限列表,无需每次回调。
type Claims struct {
TokenType string `json:"tokenType"`
TokenVersion int64 `json:"tokenVersion"`
UserId int64 `json:"userId"`
Username string `json:"username"`
ProductCode string `json:"productCode"`
MemberType string `json:"memberType"`
Perms []string `json:"perms"`
jwt.RegisteredClaims
}
tokenType字段区分"access"和"refresh",验证时应检查tokenType == "access"tokenVersion用于令牌吊销:用户注销或刷新令牌时版本递增,旧版本令牌即时失效
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
claims := middleware.GetClaims(r.Context())
if !hasPermission(claims.Perms, "order:create") {
// 返回 403
return
}
// 执行业务逻辑...
}
POST /api/auth/refreshToken
Authorization: Bearer eyJhbGciOi... # refreshToken 放在 header 中
Content-Type: application/json
{"productCode": "crm"}
refreshToken有效期默认 7 天。系统采用令牌轮转策略:每次刷新都返回全新的 accessToken 和 refreshToken,旧令牌即时失效(通过递增tokenVersion实现)。过期后必须重新登录。
appKey 和 appSecret/api/perm/sync 同步了全部权限AccessSecrettokenType == "access"claims.Perms 做了权限校验accessToken 过期时能自动调用 refreshToken 续期(注意:刷新后旧令牌失效,需立即替换)/api/auth/logout),确保令牌在服务端即时吊销所有接口基础路径为 /api,请求方式统一为 POST,参数通过 JSON Body 传递。需鉴权接口须携带 Authorization: Bearer {accessToken}。
所有 HTTP 接口均返回统一的 JSON 结构:
{
"code": 0,
"msg": "ok",
"data": { ... }
}
| 字段 | 类型 | 说明 |
|---|---|---|
code |
int | 业务状态码,0 表示成功,非零表示失败 |
msg |
string | 状态描述 |
data |
object/null | 业务数据,失败时无此字段 |
| Code | 语义 | 典型场景 |
|---|---|---|
0 |
成功 | 所有正常响应 |
400 |
请求不合法 | 参数缺失/格式错误、原密码错误、存在子部门无法删除等 |
401 |
未认证 | 未登录、token 无效/过期/类型错误、用户名密码错误 |
403 |
无权限 | 账号已冻结、产品已禁用、非超管操作产品/部门、非管理员操作角色/用户、跨部门/越级管理 |
404 |
资源不存在 | 用户/产品/角色/部门/成员不存在 |
409 |
资源冲突 | 用户名已存在、产品编码已存在、角色名重复、成员重复添加 |
500 |
系统错误 | 数据库异常等未预期的内部错误(不暴露具体信息) |
生成一张数字图片验证码,返回验证码 ID 和 Base64 编码的图片数据。仅在 cap.js 未启用时使用,前端应先调用 /api/capjs/endpoint 判断当前验证方式。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| width | int | 否 | 图片宽度(像素),默认 240 |
| height | int | 否 | 图片高度(像素),默认 80 |
响应 data: {"id": "xxx", "base64image": "data:image/png;base64,..."}
返回 cap.js 人机验证服务的前端接入地址。前端应在展示登录页前调用此接口,据返回值决定显示图片验证码还是 cap.js 挂件。
无请求参数。
响应 data: {"data": "https://cap.example.com/sitekey/"} 或 {"data": ""} (空串表示 cap.js 未启用,降级为图片验证码)
产品终端用户的统一登录入口。用户通过用户名密码 + 产品编码登录指定产品,登录成功后获得 JWT 令牌对和该用户在此产品下的权限列表。
调用场景:
安全约束:
/auth/adminLogin),防止超管凭据在产品端暴露status=1),且账号未冻结/auth/login/cap;cap.js 未启用时必须携带图片验证码(captchaId + captchaCode)| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| productCode | string | 是 | 产品编码 |
| captchaId | string | 条件必填 | 图片验证码 ID(cap.js 未启用时必填) |
| captchaCode | string | 条件必填 | 图片验证码答案(cap.js 未启用时必填) |
| 字段 | 类型 | 说明 |
|---|---|---|
| accessToken | string | 访问令牌(用于后续 API 调用,默认 2 小时有效) |
| refreshToken | string | 刷新令牌(用于续期,默认 7 天有效) |
| expires | int64 | accessToken 过期时间(Unix 时间戳,秒) |
| userInfo | object | 用户信息(含 perms 权限码数组,前端据此控制菜单/按钮显隐) |
产品端使用 cap.js 人机验证令牌登录。前端从 cap.js 挂件获得 capToken 后调用此接口,服务端向 cap.js 服务端验证令牌后执行与 /auth/login 相同的业务逻辑。
前提条件: 服务端必须配置并启用 cap.js(Capjs.Enable=1),否则返回 400。
安全约束: 与 /auth/login 相同,另外 cap.js 未启用时此接口直接拒绝请求,防止绕过验证。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| productCode | string | 是 | 产品编码 |
| capToken | string | 是 | cap.js 前端挂件返回的验证令牌 |
响应 data: 与 /auth/login 相同。
权限管理系统自身管理后台的登录入口。仅限超级管理员使用,必须额外提供 managementKey 验证身份。
调用场景:
安全约束:
managementKey,该密钥仅管理后台前端持有,不可泄露给产品端managementKey 校验在限流之前执行,无效密钥不会消耗限流配额,防止 DoS 攻击/auth/adminLogin/cap;cap.js 未启用时必须携带图片验证码(captchaId + captchaCode)| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| managementKey | string | 是 | 管理端密钥(配置文件中的 Auth.ManagementKey) |
| captchaId | string | 条件必填 | 图片验证码 ID(cap.js 未启用时必填) |
| captchaCode | string | 条件必填 | 图片验证码答案(cap.js 未启用时必填) |
响应 data: 与产品端登录接口相同。登录后不携带产品上下文,token 中 productCode 和 perms 为空。超管拥有所有产品的全部权限,不需要在 token 中枚举。
管理后台使用 cap.js 人机验证令牌登录。与 /auth/adminLogin 业务逻辑相同,区别在于以 cap.js 令牌代替图片验证码完成人机验证。
前提条件: 服务端必须配置并启用 cap.js(Capjs.Enable=1),否则返回 400。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| managementKey | string | 是 | 管理端密钥 |
| capToken | string | 是 | cap.js 前端挂件返回的验证令牌 |
响应 data: 与 /auth/adminLogin 相同。
使用有效的 refreshToken 换取全新的令牌对。采用令牌轮转策略,每次刷新后旧令牌即时失效,确保单会话安全。
调用场景:
productCode 切换到特定产品的权限视角,获取该产品下的权限列表安全约束:
Authorization: Bearer {refreshToken} 请求头传入,必须是 tokenType=refresh 的令牌tokenVersion,旧的 access/refresh 令牌即时失效——即使令牌被窃取,攻击者也只能用一次| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| Authorization | header | 是 | Bearer {refreshToken} |
| productCode | string | 否 | 切换产品上下文时传入(Body) |
响应 data: 与登录接口相同,包含全新的 accessToken + refreshToken + userInfo。
产品后端全量上报权限列表,系统自动对比差异执行新增、更新和禁用操作。这是产品权限声明的唯一入口,管理员无需手动创建权限。
调用场景:
log.Fatal 终止启动处理逻辑:
code 匹配,更新 name 和 remarkstatus=2),不删除——已绑定到角色的记录保留但权限计算时过滤安全约束: 通过 appKey/appSecret 认证(非 JWT),无需登录,受 IP 维度限流保护
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appKey | string | 是 | 产品接入密钥 |
| appSecret | string | 是 | 产品签名密钥 |
| perms | array | 是 | 权限列表(全量,不在列表中的已有权限将被禁用) |
| perms[].code | string | 是 | 权限码(如 user:create,产品内唯一) |
| perms[].name | string | 是 | 权限名(用于管理后台展示) |
| perms[].remark | string | 否 | 备注 |
响应 data: {"added": 3, "updated": 1, "disabled": 0}
主动吊销当前用户的所有已签发令牌,使其在所有设备/浏览器上立即失效。
调用场景:
处理逻辑: 递增当前用户的 tokenVersion,所有持有旧版本的 access/refresh 令牌在下次使用时都会被拒绝;同时清除 UserDetailsLoader 缓存。
无请求参数。响应 data: null
返回当前登录用户的完整信息,包括基本资料、部门归属、产品成员身份和权限列表。数据来自 UserDetailsLoader 缓存。
调用场景:
无请求参数。响应 data: UserInfo 对象(含 userId、username、nickname、avatar、email、phone、deptName、memberType、perms 等)。
用户修改自己的登录密码。修改成功后递增 tokenVersion,所有已签发令牌即时失效,用户需重新登录。
调用场景:
mustChangePassword 标志)安全约束: 必须验证原密码正确后才能修改;新密码需 6-72 字符且不能与旧密码相同。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| oldPassword | string | 是 | 原密码 |
| newPassword | string | 是 | 新密码(6-72 字符,不能与旧密码相同) |
已登录用户修改自己的昵称、头像、邮箱、手机号。userId 从 JWT 令牌中提取,不接受客户端传入,防止越权修改他人信息。
调用场景:
安全约束:
updateTime CAS)防止并发覆盖| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| nickname | string | 否 | 昵称(上限 64 字符) |
| avatar | string | 否 | 头像 URL(上限 255 字符) |
| string | 否 | 邮箱(上限 64 字符) | |
| phone | string | 否 | 手机号(上限 32 字符) |
响应: 无 data(成功时返回标准 {"code":0,"msg":"ok"} 结构)。若信息被其他会话并发修改,返回 409 冲突错误。
为一个新的业务系统注册接入权限系统。创建后自动生成 appKey/appSecret(用于权限同步认证)和初始 ADMIN 管理员账号。
调用场景:
注意事项: 响应中直接返回 appKey 和 adminUser,但 appSecret 与初始 adminPassword 不在响应体中——响应携带一次性 credentialsTicket(5 分钟有效),需立即调用 /api/product/fetchInitialCredentials 领取完整凭证。ticket 一次消费即失效,过期或已使用后无法重新获取。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | string | 是 | 产品编码(全局唯一,仅允许字母/数字/下划线/中划线,不能以数字开头,上限 64 字符) |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
| adminDeptId | int64 | 是 | 初始管理员账号所属部门 ID(归属研发部门的管理员将自动拥有全部权限) |
响应 data: {"id", "code", "appKey", "adminUser", "credentialsTicket", "credentialsExpiresAt"}
修改产品的名称、备注或启用/禁用状态。
调用场景:
status=2)。禁用后该产品下所有在线用户的令牌即时失效(JWT 中间件和 gRPC 接口均拦截),任何登录/刷新请求都将被拒绝status=1)副作用: 更新操作会清除该产品下所有用户的 UserDetailsLoader 缓存(CleanByProduct),确保状态变更即时生效。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 产品 ID |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 1=启用 2=禁用 |
分页查询所有已注册的产品。
调用场景:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | int64 | 否 | 页码,默认 1 |
| pageSize | int64 | 否 | 每页条数,默认 20,上限 100 |
响应 data: {"total": N, "list": [ProductItem...]}
查询单个产品的完整信息(含 appKey,不含 appSecret)。
调用场景:
appKey(appSecret 仅创建时返回,此接口不包含)| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 产品 ID |
安全地获取产品创建时生成的初始凭证(appKey、appSecret、初始管理员账号密码)。采用一次性 ticket 机制,凭证仅可获取一次。
调用场景:
安全约束:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ticket | string | 是 | 创建产品时返回的一次性凭证票据 |
响应 data: {"appKey": "...", "appSecret": "...", "adminUser": "...", "adminPassword": "..."}
在组织架构中新增一个部门节点。部门是全局概念,不隶属于任何产品。
调用场景:
deptType=DEV 的部门中的成员加入任何产品后自动拥有全部权限,适用于需要跨产品调试的研发团队| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| parentId | int64 | 是 | 父部门 ID,0 表示顶级部门 |
| name | string | 是 | 部门名称 |
| sort | int64 | 否 | 排序值(同级部门间的排列顺序,数值越小越靠前) |
| deptType | string | 否 | NORMAL(默认)或 DEV(研发部门,成员在产品中自动拥有全部权限) |
| remark | string | 否 | 备注 |
响应 data: {"id": 1}
修改部门名称、排序、类型或启禁用状态。
调用场景:
NORMAL → DEV),或将研发部门降级为普通部门。类型变更立即影响该部门下所有用户在各产品中的权限副作用: 更新操作会清除该部门下所有用户的 UserDetailsLoader 缓存,确保部门类型或状态变更即时反映到权限计算中。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 部门 ID |
| name | string | 是 | 名称 |
| sort | int64 | 否 | 排序值 |
| deptType | string | 否 | NORMAL 或 DEV |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 状态 |
永久删除一个部门。存在子部门或关联用户时拒绝删除,必须先迁移或删除子部门及部门下的用户。
调用场景:
约束: 存在子部门或关联用户时返回 400 错误,防止意外删除整个子树。删除前应先通过部门树接口确认该部门无子节点且无用户归属。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 部门 ID |
返回完整的部门树形结构,包含所有层级的嵌套 children 和 deptType 标识。
调用场景:
无请求参数。
分页查询指定产品下的所有权限定义。权限由产品后端通过 /perm/sync 自动上报,管理员无法手动创建或删除权限。
调用场景:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
响应 data: {"total": N, "list": [PermItem...]}(含 id、code、name、remark、status)
在指定产品下创建一个新角色。角色是产品级概念,同名角色在不同产品中互不干扰。
调用场景:
安全约束: 产品必须处于启用状态;permsLevel 数值越小权限越高,用于同级成员间的管理权限比较。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 所属产品编码 |
| name | string | 是 | 角色名(产品内唯一) |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级(数值越小权限越高,用于管理权限判定) |
响应 data: {"id": 1}
修改角色名称、备注、权限等级或启禁用状态。
调用场景:
permsLevel 调高或调低,影响该角色用户在管理权限检查中的级别status=2),已绑定该角色的用户在权限计算时将跳过该角色的权限安全约束: 非超级管理员不可降低角色的 permsLevel(即不可将数值改大),防止产品管理员通过降低角色级别来提升自己的相对权限。
副作用: 更新后自动清除所有绑定该角色的用户在该产品下的 UserDetailsLoader 缓存。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 角色 ID |
| name | string | 是 | 角色名 |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级 |
| status | int64 | 否 | 状态 |
永久删除一个角色,级联清理该角色的所有权限绑定(sys_role_perm)和用户绑定(sys_user_role)。
调用场景:
副作用: 删除前先查询所有绑定该角色的用户 ID 列表,删除后批量清除这些用户在该产品下的缓存,确保权限变更即时生效。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 角色 ID |
分页查询指定产品下的所有角色。
调用场景:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
查询单个角色的完整信息及其绑定的权限 ID 列表。
调用场景:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 角色 ID |
响应 data: 角色信息 + permIds(该角色绑定的权限 ID 数组)。
全量替换该角色绑定的权限列表。传入空数组清空所有绑定。
调用场景:
permIds 数组可清除该角色的所有权限绑定副作用: 操作后自动清除所有绑定该角色的用户在该产品下的缓存,确保权限变更即时反映到在线用户。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| roleId | int64 | 是 | 角色 ID |
| permIds | []int64 | 是 | 权限 ID 列表(全量替换,空数组清空绑定) |
创建一个全局用户账号。服务端自动生成符合强度要求的随机密码,通过一次性凭证票据(ticket)安全下发,避免密码明文出现在请求/响应体中。
调用场景:
deptId),后续通过成员管理授予特定产品访问权注意事项: 创建用户后还需通过 /member/add 将其加入产品才能登录使用该产品。响应中携带 credentialsTicket(5 分钟有效),需立即调用 /api/user/fetchCredentials 领取用户名和初始密码。ticket 一次消费即失效。用户首次登录后会被强制要求修改密码(mustChangePassword=1)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名(全局唯一) |
| nickname | string | 否 | 昵称 |
| string | 否 | 邮箱(需合法格式) | |
| phone | string | 否 | 手机号(7-15 位数字,可含 + 前缀) |
| remark | string | 否 | 备注 |
| deptId | int64 | 否 | 部门 ID(归属研发部门的用户加入产品后自动获得全部权限) |
响应 data: {"id": 1, "credentialsTicket": "...", "credentialsExpiresAt": 1716000000}
管理员为指定用户重置密码。服务端生成新的随机强密码,通过一次性凭证票据安全下发。重置后旧令牌即时失效(tokenVersion 递增),用户下次登录需使用新密码且会被强制要求修改密码。
调用场景:
安全约束:
/auth/changePassword 自行修改)CheckManageAccess 权限检查(防止越权重置高级别用户)tokenVersion,所有已签发令牌即时失效| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 目标用户 ID |
响应 data: {"credentialsTicket": "...", "credentialsExpiresAt": 1716000000}
安全地获取用户创建或密码重置时生成的凭证(用户名 + 密码)。采用一次性 ticket 机制,凭证仅可获取一次。
调用场景:
安全约束:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ticket | string | 是 | 创建用户或重置密码时返回的一次性凭证票据 |
响应 data: {"username": "zhangsan", "password": "Ax7#kL9m..."}
修改用户个人信息(昵称、邮箱、手机、部门等)。使用乐观锁防止并发更新冲突。
调用场景:
deptId)。部门变更会影响权限——从研发部门转出的用户将失去自动全权限特权deptId=0 将用户从部门中移出字段更新规则: 支持字段清空——传 "" 清空字符串字段,传 0 清空 deptId,不传字段则不更新。
副作用: 更新后清除该用户所有产品的 UserDetailsLoader 缓存。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 用户 ID |
| nickname | *string | 否 | 昵称 |
| *string | 否 | 邮箱 | |
| phone | *string | 否 | 手机号 |
| remark | *string | 否 | 备注 |
| deptId | *int64 | 否 | 部门 ID(传 0 取消部门归属) |
| status | int64 | 否 | 状态 |
分页查询用户列表,支持按用户名、昵称、状态、部门筛选,可选传入产品编码以附带成员身份信息。
调用场景:
productCode 时,列表中每个用户会附带其在该产品中的 memberType(ADMIN/DEVELOPER/MEMBER/空),方便管理员了解哪些用户已经是该产品的成员| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 否 | 产品编码(传入则每个用户附带在该产品中的成员类型) |
| username | string | 否 | 用户名(模糊匹配) |
| nickname | string | 否 | 昵称(模糊匹配) |
| status | int64 | 否 | 状态筛选(1=启用,2=禁用) |
| deptId | *int64 | 否 | 部门 ID(0 表示无部门) |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
查询单个用户的完整信息及其在当前产品下绑定的角色 ID 列表。
调用场景:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 用户 ID |
| productCode | string | 否 | 产品编码;仅 SUPER_ADMIN 有效,传入时返回该用户在指定产品下绑定的角色 ID;不传时返回该用户在所有产品下的全量角色 ID。非超管始终使用 JWT 中的产品上下文 |
响应 data: 用户信息 + roleIds(超管不传 productCode 时为跨所有产品的角色 ID;否则为指定产品下的角色 ID)。
全量替换该用户在当前产品下的角色绑定。需通过 CheckManageAccess 权限检查。
调用场景:
roleIds 数组清除用户的所有角色绑定安全约束: 操作者需要对目标用户有管理权限(通过 CheckManageAccess 校验超管/部门层级/权限级别);目标用户的成员状态必须启用。
副作用: 操作后清除目标用户在该产品下的 UserDetailsLoader 缓存。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 用户 ID |
| roleIds | []int64 | 是 | 角色 ID 列表(全量替换) |
| productCode | string | 条件必填 | 产品编码;仅 SUPER_ADMIN 有效,超管必须显式传入(为空时返回 400),非超管忽略此字段始终使用 JWT 中的产品上下文 |
全量替换用户在当前产品下的个性化 ALLOW/DENY 权限覆盖。这是在角色权限基础上的微调机制。需通过 CheckManageAccess 权限检查。
调用场景:
ALLOWDENYperms 数组清除所有个性化覆盖,用户权限完全由角色决定权限计算: 最终权限 = (角色权限 ∪ ALLOW) - DENY。DENY 优先级最高。
安全约束: 目标用户的成员状态必须启用;产品必须处于启用状态。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 用户 ID |
| perms | array | 是 | 权限覆盖列表(全量替换,空数组清除所有覆盖) |
| perms[].permId | int64 | 是 | 权限 ID |
| perms[].effect | string | 是 | ALLOW(额外授予)或 DENY(强制拒绝) |
查询指定用户在当前产品下已配置的个性化 ALLOW/DENY 覆盖记录。供管理后台在打开「设置权限」抽屉时回填当前状态,使编辑页能正确展示现有的授权/拒绝项。
调用场景:
访问控制:
RequireProductAdminFor),再通过 CheckManageAccess 校验是否有权管理目标用户,防止普通成员枚举其他用户 ID| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 目标用户 ID |
productCode从 JWT 中间件注入的产品上下文获取,无需在请求体传入。
响应 data: {"perms": [{"permId": 1, "effect": "ALLOW"}, {"permId": 5, "effect": "DENY"}, ...]}
仅返回状态为启用(status=1)的权限项的覆盖记录。前端须结合用户的角色继承权限 (/user/detail 中的 roleIds → /role/detail 中的 permIds) 计算最终有效选中状态:有效 = 角色继承 ∪ ALLOW - DENY。
冻结或解冻用户账号。冻结后用户立即无法访问任何产品。需通过 CheckManageAccess 权限检查。
调用场景:
安全约束: 不允许冻结自己;不允许冻结超级管理员;需要对目标用户有管理权限。
副作用: 状态变更后递增该用户的 tokenVersion(使所有已签发令牌失效)并清除缓存。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 用户 ID |
| status | int64 | 是 | 1=正常 2=冻结 |
将一个已有用户添加为指定产品的成员,并指定成员类型。用户成为产品成员后才能登录该产品。
调用场景:
安全约束:
CheckManageAccess + CheckMemberTypeAssignment 权限检查| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| userId | int64 | 是 | 用户 ID |
| memberType | string | 是 | ADMIN(产品管理员)/ DEVELOPER(开发者,全权限)/ MEMBER(普通成员,按角色授权) |
响应 data: {"id": 1}
修改成员类型或启禁用成员状态。
调用场景:
安全约束:
CheckManageAccess + CheckMemberTypeAssignment 权限检查副作用: 更新后清除该用户在该产品下的 UserDetailsLoader 缓存。若操作属于"降权"(ADMIN/DEVELOPER → MEMBER,或启用 → 禁用),还会递增该用户的 tokenVersion 并失效底层用户缓存,使其已签发的令牌在下次请求时被强制踢出,须重新登录。升权路径(MEMBER → ADMIN 等)不触发强制重登。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 成员记录 ID |
| memberType | string | 否 | 成员类型(不传则不修改) |
| status | int64 | 否 | 状态(不传则不修改);两字段均不传时返回 400 |
将成员从产品中移除。移除后用户无法再登录该产品,其在该产品下的所有角色绑定和权限覆盖被级联清理。
调用场景:
安全约束:
CheckManageAccess 权限检查级联清理: 在事务内同时清理 sys_user_role(该用户在该产品下的角色绑定)和 sys_user_perm(该用户在该产品下的权限覆盖),然后删除成员记录并清除缓存。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 成员记录 ID |
分页查询指定产品下的所有成员,含用户基本信息和成员类型/状态。
调用场景:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
响应 data: {"total": N, "list": [MemberItem...]}(含 userId、username、nickname、memberType、status 等)
查询指定用户作为成员加入的所有产品,含成员类型和状态。
调用场景:
访问控制:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 目标用户 ID |
响应 data: {"list": [{"productCode", "productName", "memberType", "status"}]}
将文件上传到 MinIO 对象存储,返回访问 URL 和文件路径。支持按 fileType 分类存储到不同目录,自动去重(基于 MD5)。
调用场景:
fileType 传 avatarfileType 上传不同类型的文件到对应目录请求格式: multipart/form-data(非 JSON),最大 32MB
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | file | 是 | 上传的文件 |
| fileType | string | 是 | 文件类型标识(需在服务端 Minio 配置中预定义,如 avatar) |
安全约束:
fileType 必须在服务端配置的允许列表中,未配置的类型拒绝上传fileType 配置允许的 Content-Type 白名单(如头像仅允许 image/jpeg、image/png)响应 data: {"url": "https://...", "path": "avatar/2026/05/13/xxx.png", "md5": "...", "size": 102400}
gRPC 服务定义见 pb/perm.proto,默认监听 :10002。所有 gRPC 错误使用标准 status.Error(codes.Xxx, msg) 格式。
gRPC 接口主要面向产品后端服务间调用,相比 HTTP 接口具有更高性能和更强的类型安全。推荐 Go 项目通过 permclient 包直接调用。
产品后端在启动阶段全量上报权限列表,与 HTTP 的 /perm/sync 功能等价。
调用场景:
main 函数中通过 gRPC 调用同步权限,相比 HTTP 调用减少序列化开销认证方式: 通过 appKey/appSecret 认证(与 HTTP 接口一致)。
产品后端代理终端用户进行身份认证,返回 JWT 令牌对和权限列表。
调用场景:
安全约束: productCode 必传;超级管理员被拒绝(必须走 adminLogin)。
使用 refreshToken 换取全新令牌对,采用令牌轮转策略。
调用场景:
安全约束: 每次刷新递增 tokenVersion,旧令牌即时失效;产品禁用时返回 codes.PermissionDenied。
服务端验证 accessToken 的有效性,返回用户信息和权限列表。
调用场景:
推荐实践: 常规场景推荐本地 JWT 验证(零网络开销);仅在上述特殊场景使用此接口。
安全约束: 产品禁用时返回 codes.PermissionDenied。
实时查询指定用户在指定产品下的最新权限码列表。
调用场景:
import "perms-system-server/permclient"
client, err := permclient.NewPermClient("perm-system:10002")
resp, err := client.SyncPermissions(ctx, &pb.SyncPermissionsReq{...})
resp, err := client.Login(ctx, &pb.LoginReq{...})
resp, err := client.VerifyToken(ctx, &pb.VerifyTokenReq{AccessToken: token})
resp, err := client.GetUserPerms(ctx, &pb.GetUserPermsReq{...})
server/
├── perm.go # 入口(同时启动 HTTP + gRPC)
├── perm.api # HTTP API 定义(go-zero 标准)
├── perm.sql # 数据库 DDL
├── gen-api.sh # API 代码生成脚本
├── gen-model.sh # Model 代码生成脚本
├── run-test.sh # 测试运行脚本
├── test-design.md # 测试用例设计文档(1065 TC)
├── test-report.md # 测试报告
├── etc/
│ └── perm-api.yaml # 服务配置
├── pb/
│ ├── perm.proto # gRPC 接口定义
│ ├── perm.pb.go # protoc 生成
│ └── perm_grpc.pb.go # protoc 生成
├── permclient/
│ └── permclient.go # gRPC 客户端 SDK
├── cli/goctl/ # 自定义 goctl 模板
│ ├── api/handler.tpl # Handler 模板(含参数校验 → 400 处理)
│ └── model/ # Model 模板(含 *WithTx 事务查询方法)
└── internal/
├── config/config.go # 配置结构体
├── consts/consts.go # 全局常量(Status/MemberType/DeptType/TokenType 等)
├── response/response.go # 统一响应 + 错误码
├── util/ # 工具函数(分页、校验、JWT)
├── loaders/
│ └── userDetailsLoader.go # 用户详情加载器(Redis 缓存 + DB 回源)
├── middleware/
│ └── jwtauthMiddleware.go # JWT 鉴权中间件(token/tokenVersion 验证 + 用户/产品/成员状态检查 + UserDetails 注入)
├── svc/serviceContext.go # 依赖注入容器
├── server/permserver.go # gRPC 服务实现
├── types/types.go # 请求/响应结构体(goctl 生成)
├── handler/ # HTTP handler(按模块分组,goctl 生成)
│ ├── routes.go
│ ├── pub/ # 公开接口(登录/刷新令牌/同步权限)
│ ├── auth/ # 认证接口(用户信息/修改密码/注销)
│ ├── product/ # 产品管理
│ ├── dept/ # 部门管理
│ ├── perm/ # 权限管理
│ ├── role/ # 角色管理
│ ├── user/ # 用户管理
│ └── member/ # 产品成员
├── logic/ # 业务逻辑层(按模块分组)
│ ├── auth/
│ │ ├── jwt.go # JWT 生成/解析(含 tokenType 区分)
│ │ ├── perms.go # 权限计算核心逻辑
│ │ └── access.go # 集中式操作权限控制(超管/管理员/部门/级别检查)
│ ├── pub/ # 登录、刷新、同步
│ ├── product/ # 产品 CRUD
│ ├── dept/ # 部门 CRUD + 树
│ ├── perm/ # 权限列表
│ ├── role/ # 角色 CRUD + 绑定权限 + 级联删除
│ ├── user/ # 用户 CRUD + 绑定角色/权限
│ └── member/ # 成员管理 + 级联清理
└── model/ # 数据模型层(每张表独立目录)
├── models.go # Models 聚合结构体(统一初始化所有 Model 实例)
├── product/ # sys_product
├── dept/ # sys_dept
├── perm/ # sys_perm
├── role/ # sys_role
├── roleperm/ # sys_role_perm
├── user/ # sys_user
├── userperm/ # sys_user_perm
├── userrole/ # sys_user_role
└── productmember/ # sys_product_member
配置文件位于 etc/perm-api.yaml:
| 配置项 | 说明 | 默认值 |
|---|---|---|
Host / Port |
HTTP 监听地址 | 0.0.0.0:10001 |
RpcServerConf.ListenOn |
gRPC 监听地址 | 0.0.0.0:10002 |
MySQL.DataSource |
MySQL 连接串 | — |
CacheRedis[].Host |
Redis 地址 | 127.0.0.1:6379 |
Auth.AccessSecret |
JWT accessToken 签名密钥 | — |
Auth.AccessExpire |
accessToken 有效期(秒) | 7200(2h) |
Auth.RefreshSecret |
JWT refreshToken 签名密钥 | — |
Auth.RefreshExpire |
refreshToken 有效期(秒) | 604800(7d) |
Auth.ManagementKey |
管理后台登录密钥(/auth/adminLogin 接口验证) |
— |
BehindProxy |
是否部署在反向代理(Nginx 等)后面 | false |
Capjs.Enable |
cap.js 人机验证开关(1 启用,0 或不配置则使用图片验证码) |
0 |
Capjs.EndpointURL |
cap.js 服务地址(如 https://cap.example.com) |
— |
Capjs.Key |
cap.js site key(前端挂件加载和 siteverify 路径使用) | — |
Capjs.Secret |
cap.js site secret(服务端 siteverify 验证使用,不可暴露给前端) | — |
生产环境部署前,务必修改
AccessSecret、RefreshSecret和ManagementKey为安全的随机字符串,并确保产品后端的本地验证密钥与AccessSecret一致。ManagementKey仅管理后台前端持有,不可泄露给产品端。启用 cap.js 时,Capjs.Secret同样属于服务端私密配置,不得暴露给前端。
当服务部署在 Nginx 等反向代理后面时,需要开启 BehindProxy 配置以正确获取客户端真实 IP(用于登录限流等安全策略):
BehindProxy: true
必须在 Nginx 中正确设置 X-Real-IP 并清除客户端可伪造的 X-Forwarded-For:
server {
location /api/ {
proxy_pass http://127.0.0.1:10001;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For "";
proxy_set_header Host $host;
}
}
BehindProxy: false(默认)— 仅使用 TCP 连接的 RemoteAddr 作为客户端 IP,适合服务直接暴露或在无法信任代理头的场景BehindProxy: true — 信任 Nginx 设置的 X-Real-IP 头获取真实客户端 IP,必须确保 Nginx 正确配置且外部无法绕过 Nginx 直连后端| 指标 | 数值 |
|---|---|
| 测试用例总数 | 1065 |
| 已覆盖 TC | 1065(100%) |
| 测试函数 | 1170 |
| 测试子用例 | 1309 |
| 测试包 | 28 |
| 通过率 | 100% |
测试覆盖范围:
| 文件 | 说明 |
|---|---|
test-design.md |
测试用例设计文档(1065 个 TC,含测试场景、输入数据、预期结果) |
test-report.md |
测试执行报告(含各包耗时、TC 明细、源码审计结果) |
项目提供 run-test.sh 脚本,自动完成以下流程:
go build ./...,编译失败则不跑测试go test,结束后汇总通过/失败的包数和耗时# 运行全部测试
./run-test.sh
# 运行指定包的测试
./run-test.sh ./internal/logic/auth/...
./run-test.sh ./internal/model/...
# 查看帮助
./run-test.sh -h
| 变量 | 默认值 | 说明 |
|---|---|---|
TEST_VERBOSE |
空(关闭) | 设为 1 开启详细输出(-v) |
TEST_RUN |
空(全部) | 只运行匹配的测试函数名 |
TEST_TIMEOUT |
600s |
单个包的超时时间 |
TEST_COUNT |
1 |
每个测试运行次数 |
# 组合使用示例
TEST_VERBOSE=1 TEST_RUN="TestLogin" ./run-test.sh ./internal/logic/pub/...
测试文件遵循 Go 标准命名,与被测文件同目录:
internal/logic/auth/
├── jwt.go # 源码
├── jwt_test.go # 测试
├── perms.go # 源码
├── perms_test.go # 测试
└── perms_mock_test.go # Mock 测试
internal/model/product/
├── sysProductModel.go # 自定义 Model
├── sysProductModel_gen.go # 生成的 Model
└── sysProductModel_test.go # 测试
本项目严格遵循 go-zero 标准开发流程:
flowchart LR
A[编辑 perm.api] --> B["./gen-api.sh"]
B --> C[生成 handler + types + routes]
C --> D[在 logic 中填充业务逻辑]
D --> E[go build 验证]
F[编辑 perm.sql] --> G["./gen-model.sh"]
G --> H[生成 model 到对应子目录]
H --> I[在 sysXxxModel.go 中扩展自定义查询]
# 1. 在 perm.api 中添加类型和路由定义
# 2. 重新生成(已有 logic 文件不会被覆盖)
./gen-api.sh
# 3. 在生成的 logic 文件中填写业务逻辑
# 1. 在 perm.sql 中添加建表语句
# 2. 生成 model(使用自定义模板,包含 *WithTx 事务查询方法)
./gen-model.sh
# 3. 在生成的 sysXxxModel.go 中添加自定义查询方法
项目使用自定义模板(cli/goctl/),主要增强:
400 Bad Request(而非 500)FindOne 和 FindOneByXxx 方法均提供 *WithTx 事务变体,确保事务内查询使用 session 连接