|
|
4 minggu lalu | |
|---|---|---|
| cli | 1 bulan lalu | |
| etc | 1 bulan lalu | |
| internal | 4 minggu lalu | |
| pb | 4 minggu lalu | |
| permclient | 1 bulan lalu | |
| .gitignore | 1 bulan lalu | |
| .gitmodules | 1 bulan lalu | |
| README.md | 4 minggu lalu | |
| audit-report.md | 4 minggu lalu | |
| gen-api.sh | 1 bulan lalu | |
| gen-model.sh | 4 minggu lalu | |
| go.mod | 1 bulan lalu | |
| go.sum | 1 bulan lalu | |
| perm.api | 1 bulan lalu | |
| perm.go | 4 minggu lalu | |
| perm.sql | 4 minggu lalu | |
| run-test.sh | 1 bulan lalu | |
| test-design.md | 4 minggu lalu | |
| test-report.md | 4 minggu lalu |
集中式多产品权限管理平台后端服务。为多个产品提供统一的用户认证、权限管理、角色管理能力,产品通过 HTTP API 或 gRPC 接入。
多产品隔离 — 权限、角色、成员按产品(productCode)隔离,一个账号可跨产品通用
灵活的权限模型 — 角色权限 + 用户级 ALLOW/DENY 覆盖,细粒度控制
研发部门自动授权 — 研发部门(deptType=DEV)的成员加入产品后自动拥有全部权限
双协议 — 同时提供 HTTP REST API(管理 UI)和 gRPC(产品后端高性能调用)
自动权限同步 — 产品启动时通过 API 自动上报权限列表,系统自动新增/更新/禁用
JWT 本地验证 — 登录获取 JWT,产品后端可本地验证,无需每次请求回调权限系统
登录端隔离 — 产品端(/auth/login)和管理后台(/auth/adminLogin)独立登录接口,超管仅能通过管理后台登录
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[获取用户权限] --> 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),该账号是该产品的 SUPER_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", "password": "123456", "nickname": "张三", "deptId": 1}
# 返回 userId=10
# 将张三添加为 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", "password": "123456", "nickname": "李四", "deptId": 2}
# 返回 userId=11
POST /api/member/add {"productCode": "crm", "userId": 11, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 11, "roleIds": [2]} # 分配"普通销售"角色
李四登录 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", "password": "123456", "nickname": "外包王五"}
# deptId 不传,不归属任何部门
POST /api/member/add {"productCode": "crm", "userId": 12, "memberType": "MEMBER"}
POST /api/user/bindRoles {"userId": 12, "roleIds": [3]} # 分配"客服"角色(只读)
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>Status != 1 → 403"]
L3 --> L4["④ Logic 层<br>操作权限控制"]
L4 --> BIZ[业务逻辑]
| 层级 | 组件 | 职责 |
|---|---|---|
| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 tokenType |
| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限) |
| 第三层 | 用户状态检查 | 冻结账号(status ≠ 1)直接返回 403 |
| 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 |
| 接口 | 权限要求 | 额外检查 |
|---|---|---|
| 产品管理 | ||
| 创建产品 | 仅超级管理员 | — |
| 更新产品 | 仅超级管理员 | — |
| 部门管理 | ||
| 创建部门 | 仅超级管理员 | — |
| 更新部门 | 仅超级管理员 | — |
| 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
| 角色管理 | ||
| 创建角色 | 超管 或 产品管理员 | — |
| 更新角色 | 超管 或 产品管理员 | — |
| 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
| 绑定角色权限 | 超管 或 产品管理员 | — |
| 用户管理 | ||
| 创建用户 | 超管 或 产品管理员 | — |
| 更新用户信息 | 仅本人 或 超管 | — |
| 冻结/解冻用户 | 通过 CheckManageAccess |
不可冻结自己和超管 |
| 绑定角色 | 通过 CheckManageAccess |
— |
| 设置权限覆盖 | 通过 CheckManageAccess |
— |
| 成员管理 | ||
| 添加成员 | 通过 CheckManageAccess + CheckMemberTypeAssignment |
不可分配同级或更高类型 |
| 更新成员 | 通过 CheckManageAccess + CheckMemberTypeAssignment |
— |
| 移除成员 | 通过 CheckManageAccess |
级联清理角色/权限绑定 |
| 查询类接口 | ||
| 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
| 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
| 公开接口 | ||
| 产品端登录 (login) | 无需鉴权 | 超级管理员被拒绝,productCode 必传 |
| 管理后台登录 (adminLogin) | 无需鉴权 | 需验证 managementKey |
| 刷新令牌 / 同步权限 | 无需鉴权 | 同步权限通过 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 |
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 |
该部门下所有用户的缓存 |
# 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": "客户关系管理"}
响应中包含 appKey、appSecret、adminUser、adminPassword,请立即保存,后续不再展示。
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": "导出报表"}
]
}
Go 代码示例(放在 main 启动流程中):
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"`
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"。
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 天,刷新时返回原始 refreshToken(不重新签发),过期后必须重新登录。这确保了 token 有固定的生命周期。
appKey 和 appSecret/api/perm/sync 同步了全部权限AccessSecrettokenType == "access"claims.Perms 做了权限校验accessToken 过期时能自动调用 refreshToken 续期所有接口基础路径为 /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 |
系统错误 | 数据库异常等未预期的内部错误(不暴露具体信息) |
供产品后端调用,超级管理员无法通过此接口登录。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| productCode | string | 是 | 产品编码 |
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
| accessToken | string | 访问令牌 |
| refreshToken | string | 刷新令牌 |
| expires | int64 | accessToken 过期时间(Unix 时间戳,秒) |
| userInfo | object | 用户信息(含 perms 权限码数组) |
仅供权限系统管理后台使用,需要传入配置的 managementKey 进行身份验证。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名 |
| password | string | 是 | 密码 |
| managementKey | string | 是 | 管理端密钥(配置文件中的 Auth.ManagementKey) |
响应 data: 与产品端登录接口相同。登录后不携带产品上下文,token 中 productCode 和 perms 为空。
通过 Authorization: Bearer {refreshToken} 请求头传入 refresh token。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| Authorization | header | 是 | Bearer {refreshToken} |
| productCode | string | 否 | 切换产品上下文时传入(Body) |
响应 data: 与登录接口相同。注意返回的 refreshToken 是原始值(不重新签发),refresh token 有固定有效期,过期后需重新登录。
通过 appKey/appSecret 认证,产品后端上报全量权限列表。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appKey | string | 是 | 产品接入密钥 |
| appSecret | string | 是 | 产品签名密钥 |
| perms | array | 是 | 权限列表 |
| perms[].code | string | 是 | 权限码(如 user:create) |
| perms[].name | string | 是 | 权限名 |
| perms[].remark | string | 否 | 备注 |
响应 data: {"added": 3, "updated": 1, "disabled": 0}
无请求参数。响应 data: UserInfo 对象。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| oldPassword | string | 是 | 原密码 |
| newPassword | string | 是 | 新密码(6-72 字符,不能与旧密码相同) |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | string | 是 | 产品编码(全局唯一) |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
响应 data: {"id", "code", "appKey", "appSecret", "adminUser", "adminPassword"}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 产品 ID |
| name | string | 是 | 产品名称 |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 1=启用 2=禁用 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | int64 | 否 | 页码,默认 1 |
| pageSize | int64 | 否 | 每页条数,默认 20,上限 100 |
响应 data: {"total": N, "list": [ProductItem...]}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 产品 ID |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| parentId | int64 | 是 | 父部门 ID,0 表示顶级部门 |
| name | string | 是 | 部门名称 |
| sort | int64 | 否 | 排序值 |
| deptType | string | 否 | NORMAL(默认)或 DEV(研发部门) |
| remark | string | 否 | 备注 |
响应 data: {"id": 1}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 部门 ID |
| name | string | 是 | 名称 |
| sort | int64 | 否 | 排序值 |
| deptType | string | 否 | NORMAL 或 DEV |
| remark | string | 否 | 备注 |
| status | int64 | 否 | 状态 |
存在子部门时无法删除。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 部门 ID |
无请求参数。返回完整的部门树形结构(含 children 嵌套和 deptType)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
响应 data: {"total": N, "list": [PermItem...]}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 所属产品编码 |
| name | string | 是 | 角色名(产品内唯一) |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级 |
响应 data: {"id": 1}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 角色 ID |
| name | string | 是 | 角色名 |
| remark | string | 否 | 备注 |
| permsLevel | int64 | 是 | 权限等级 |
| status | int64 | 否 | 状态 |
级联删除角色关联的权限绑定和用户绑定。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 角色 ID |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
返回角色信息及绑定的权限 ID 列表(permIds)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 角色 ID |
全量替换该角色的权限。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| roleId | int64 | 是 | 角色 ID |
| permIds | []int64 | 是 | 权限 ID 列表(空数组清空绑定) |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 登录名(唯一) |
| password | string | 是 | 密码(6-72 字符) |
| nickname | string | 否 | 昵称 |
| string | 否 | 邮箱(需合法格式) | |
| phone | string | 否 | 手机号(7-15 位数字,可含 + 前缀) |
| remark | string | 否 | 备注 |
| deptId | int64 | 否 | 部门 ID |
响应 data: {"id": 1}
支持字段清空:传 "" 清空字符串字段,传 0 清空 deptId,不传字段则不更新。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 用户 ID |
| nickname | *string | 否 | 昵称 |
| *string | 否 | 邮箱 | |
| phone | *string | 否 | 手机号 |
| remark | *string | 否 | 备注 |
| deptId | *int64 | 否 | 部门 ID(传 0 取消部门) |
| status | int64 | 否 | 状态 |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 否 | 产品编码(传入则附带成员类型) |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
返回用户信息及绑定的角色 ID 列表(roleIds)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 用户 ID |
需通过 CheckManageAccess 权限检查。全量替换该用户的角色。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 用户 ID |
| roleIds | []int64 | 是 | 角色 ID 列表 |
需通过 CheckManageAccess 权限检查。全量替换用户级别的 ALLOW/DENY 权限覆盖。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| userId | int64 | 是 | 用户 ID |
| perms | array | 是 | 权限覆盖列表 |
| perms[].permId | int64 | 是 | 权限 ID |
| perms[].effect | string | 是 | ALLOW 或 DENY |
需通过 CheckManageAccess 权限检查。不允许冻结自己和超级管理员。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 用户 ID |
| status | int64 | 是 | 1=正常 2=冻结 |
需通过 CheckManageAccess + CheckMemberTypeAssignment 权限检查。不可分配与自己同级或更高级的成员类型。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| userId | int64 | 是 | 用户 ID |
| memberType | string | 是 | ADMIN / DEVELOPER / MEMBER |
响应 data: {"id": 1}
需通过 CheckManageAccess + CheckMemberTypeAssignment 权限检查。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 成员记录 ID |
| memberType | string | 是 | 成员类型 |
| status | int64 | 否 | 状态 |
需通过 CheckManageAccess 权限检查。级联清理该成员在该产品下的角色绑定和权限覆盖。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | int64 | 是 | 成员记录 ID |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| productCode | string | 是 | 产品编码 |
| page | int64 | 否 | 页码 |
| pageSize | int64 | 否 | 每页条数 |
响应 data: {"total": N, "list": [MemberItem...]}
gRPC 服务定义见 pb/perm.proto,默认监听 :10002。
| 方法 | 说明 | 使用场景 |
|---|---|---|
SyncPermissions |
同步产品权限列表 | 产品启动时调用 |
Login |
产品端登录 | 产品后端代理用户登录(productCode 必传,超管被拒绝) |
RefreshToken |
刷新令牌 | accessToken 过期续期 |
VerifyToken |
验证令牌 | 产品后端验证用户 token(可选,推荐本地 JWT 验证) |
GetUserPerms |
获取用户权限 | 实时查询用户最新权限 |
所有 gRPC 错误使用标准 status.Error(codes.Xxx, msg) 格式。
Go 项目可直接引用 permclient 包:
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})
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 # 测试用例设计文档(433 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 验证 + 状态检查 + 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 |
生产环境部署前,务必修改
AccessSecret、RefreshSecret和ManagementKey为安全的随机字符串,并确保产品后端的本地验证密钥与AccessSecret一致。ManagementKey仅管理后台前端持有,不可泄露给产品端。
当服务部署在 Nginx 等反向代理后面时,需要开启 BehindProxy 配置以正确获取客户端真实 IP(用于登录限流等安全策略):
1. 配置文件设置
BehindProxy: true
2. Nginx 配置要求
必须在 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 直连后端| 指标 | 数值 |
|---|---|
| 测试用例总数 | 499 |
| 已覆盖 TC | 498(99.8%) |
| 测试函数 | 662 |
| 测试子用例 | 744 |
| 测试包 | 23 |
| 通过率 | 99.7% |
测试覆盖范围:
| 文件 | 说明 |
|---|---|
test-design.md |
测试用例设计文档(499 个 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 连接