暫無描述

BaiLuoYan 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
cli c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
etc 655bc2cb51 feat: 拆分产品端登录接口和系统管理后台登录接口 1 月之前
internal 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
pb c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
permclient c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
.gitignore c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
.gitmodules c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
README.md 655bc2cb51 feat: 拆分产品端登录接口和系统管理后台登录接口 1 月之前
audit-report.md 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
gen-api.sh c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
gen-model.sh 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
go.mod 64f332a5fe feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
go.sum 64f332a5fe feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
perm.api 655bc2cb51 feat: 拆分产品端登录接口和系统管理后台登录接口 1 月之前
perm.go 64f332a5fe feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
perm.sql 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
run-test.sh c52b2cbd3f feat: 权限系统代码首次提交 1 月之前
test-design.md 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前
test-report.md 535b685218 feat: 静态代码审计,修复逻辑bug和安全漏洞 1 月之前

README.md

统一权限管理系统(Permission System Server)

集中式多产品权限管理平台后端服务。为多个产品提供统一的用户认证、权限管理、角色管理能力,产品通过 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           │        │
                              │  └───────────────────────┘        │
                              └────────────────────────────────────┘

关键关系

  1. 用户 → 产品:通过 sys_product_member 建立,一个用户可以是多个产品的成员
  2. 用户 → 部门:通过 sys_user.deptId 关联,一个用户只属于一个部门
  3. 角色 → 权限:通过 sys_role_perm 绑定,一个角色可绑定多个权限
  4. 用户 → 角色:通过 sys_user_role 分配,一个用户可拥有多个角色
  5. 用户 → 权限覆盖:通过 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[不添加为成员 = 无权限]

操作权限控制(Access Control)

系统内置一套集中式操作权限管控机制,对所有管理类接口实施多维度的访问控制,防止越权操作。

安全架构:四层纵深防护

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 — 用户管理权限检查

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[❌ 同级或更低]

检查维度说明

  1. 超管豁免SUPER_ADMIN 不受任何限制
  2. 自我豁免:操作自己的记录总是允许
  3. 部门层级:操作者只能管理本部门及下级子部门的用户(ADMIN 和超管豁免此检查)
  4. 权限级别:先比 memberType 优先级,同级再比 permsLevel 数值
    • memberType 优先级:SUPER_ADMIN(0) > ADMIN(1) > DEVELOPER(2) > MEMBER(3)
    • permsLevel:数值越小权限越高(如 10 > 20),无角色默认为 MaxInt64(最低)

UserDetailsLoader — 用户信息缓存

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 集合)

缓存策略

  • 存储:Redis JSON,key 格式 {prefix}:ud:{userId}:{productCode}
  • TTL:300 秒(5 分钟)自然过期
  • 主动失效:所有写操作均触发对应的缓存清除
操作 失效方法 失效范围
更新用户信息 / 冻结解冻 / 修改密码 Clean(userId) 该用户所有产品缓存
设置用户权限覆盖 / 添加成员 / 更新成员 Del(userId, productCode) 该用户在指定产品的缓存
更新角色 / 删除角色 / 绑定角色权限 BatchDel(userIds, productCode) 受影响用户在指定产品的缓存
更新产品 / 同步权限 CleanByProduct(productCode) 该产品下所有用户的缓存
更新部门 Clean(uid) × N 该部门下所有用户的缓存

快速开始

环境要求

  • Go 1.21+
  • MySQL 8.0+
  • Redis 6.0+
  • goctl 1.10+

部署步骤

# 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": "客户关系管理"}

响应中包含 appKeyappSecretadminUseradminPassword请立即保存,后续不再展示

阶段二:产品启动时同步权限

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("权限同步完成")
}

阶段三:管理员配置角色与用户

详见上文「实际业务配置指南」。

阶段四:用户登录与鉴权

1. 用户登录

方式 A:HTTP API
POST /api/auth/login
{"username": "zhangsan", "password": "123456", "productCode": "crm"}
方式 B:gRPC SDK(推荐 Go 项目)
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

2. 本地 JWT 验证(推荐)

产品后端配置与权限系统相同的 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"

3. 业务接口中检查权限

func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
    claims := middleware.GetClaims(r.Context())
    if !hasPermission(claims.Perms, "order:create") {
        // 返回 403
        return
    }
    // 执行业务逻辑...
}

4. Token 续期

POST /api/auth/refreshToken
Authorization: Bearer eyJhbGciOi...   # refreshToken 放在 header 中
Content-Type: application/json

{"productCode": "crm"}

refreshToken 有效期默认 7 天,刷新时返回原始 refreshToken(不重新签发),过期后必须重新登录。这确保了 token 有固定的生命周期。

接入检查清单

  • 已获取产品的 appKeyappSecret
  • 产品启动时调用 /api/perm/sync 同步了全部权限
  • 后端配置了与权限系统相同的 AccessSecret
  • 登录接口正确调用权限系统并返回 token
  • JWT 鉴权中间件已加入受保护路由,并验证 tokenType == "access"
  • 业务接口中根据 claims.Perms 做了权限校验
  • 前端在 accessToken 过期时能自动调用 refreshToken 续期

HTTP API 接口文档

所有接口基础路径为 /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 系统错误 数据库异常等未预期的内部错误(不暴露具体信息)

公开接口(无需鉴权)

POST /api/auth/login — 产品端登录

供产品后端调用,超级管理员无法通过此接口登录。

字段 类型 必填 说明
username string 登录名
password string 密码
productCode string 产品编码

响应 data:

字段 类型 说明
accessToken string 访问令牌
refreshToken string 刷新令牌
expires int64 accessToken 过期时间(Unix 时间戳,秒)
userInfo object 用户信息(含 perms 权限码数组)

POST /api/auth/adminLogin — 管理后台登录

仅供权限系统管理后台使用,需要传入配置的 managementKey 进行身份验证。

字段 类型 必填 说明
username string 登录名
password string 密码
managementKey string 管理端密钥(配置文件中的 Auth.ManagementKey

响应 data: 与产品端登录接口相同。登录后不携带产品上下文,token 中 productCodeperms 为空。

POST /api/auth/refreshToken — 刷新令牌

通过 Authorization: Bearer {refreshToken} 请求头传入 refresh token。

字段 类型 必填 说明
Authorization header Bearer {refreshToken}
productCode string 切换产品上下文时传入(Body)

响应 data: 与登录接口相同。注意返回的 refreshToken 是原始值(不重新签发),refresh token 有固定有效期,过期后需重新登录。

POST /api/perm/sync — 同步产品权限

通过 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}

认证接口(需鉴权)

POST /api/auth/userInfo — 获取当前用户信息

无请求参数。响应 data: UserInfo 对象。

POST /api/auth/changePassword — 修改密码

字段 类型 必填 说明
oldPassword string 原密码
newPassword string 新密码(6-72 字符,不能与旧密码相同)

产品管理(仅超级管理员)

POST /api/product/create — 创建产品

字段 类型 必填 说明
code string 产品编码(全局唯一)
name string 产品名称
remark string 备注

响应 data: {"id", "code", "appKey", "appSecret", "adminUser", "adminPassword"}

POST /api/product/update — 更新产品

字段 类型 必填 说明
id int64 产品 ID
name string 产品名称
remark string 备注
status int64 1=启用 2=禁用

POST /api/product/list — 产品列表

字段 类型 必填 说明
page int64 页码,默认 1
pageSize int64 每页条数,默认 20,上限 100

响应 data: {"total": N, "list": [ProductItem...]}

POST /api/product/detail — 产品详情

字段 类型 必填 说明
id int64 产品 ID

部门管理(仅超级管理员)

POST /api/dept/create — 创建部门

字段 类型 必填 说明
parentId int64 父部门 ID,0 表示顶级部门
name string 部门名称
sort int64 排序值
deptType string NORMAL(默认)或 DEV(研发部门)
remark string 备注

响应 data: {"id": 1}

POST /api/dept/update — 更新部门

字段 类型 必填 说明
id int64 部门 ID
name string 名称
sort int64 排序值
deptType string NORMALDEV
remark string 备注
status int64 状态

POST /api/dept/delete — 删除部门

存在子部门时无法删除。

字段 类型 必填 说明
id int64 部门 ID

POST /api/dept/tree — 部门树

无请求参数。返回完整的部门树形结构(含 children 嵌套和 deptType)。

权限管理

POST /api/perm/list — 权限列表

字段 类型 必填 说明
productCode string 产品编码
page int64 页码
pageSize int64 每页条数

响应 data: {"total": N, "list": [PermItem...]}

角色管理(超级管理员 或 产品管理员)

POST /api/role/create — 创建角色

字段 类型 必填 说明
productCode string 所属产品编码
name string 角色名(产品内唯一)
remark string 备注
permsLevel int64 权限等级

响应 data: {"id": 1}

POST /api/role/update — 更新角色

字段 类型 必填 说明
id int64 角色 ID
name string 角色名
remark string 备注
permsLevel int64 权限等级
status int64 状态

POST /api/role/delete — 删除角色

级联删除角色关联的权限绑定和用户绑定。

字段 类型 必填 说明
id int64 角色 ID

POST /api/role/list — 角色列表

字段 类型 必填 说明
productCode string 产品编码
page int64 页码
pageSize int64 每页条数

POST /api/role/detail — 角色详情

返回角色信息及绑定的权限 ID 列表(permIds)。

字段 类型 必填 说明
id int64 角色 ID

POST /api/role/bindPerms — 绑定角色权限

全量替换该角色的权限。

字段 类型 必填 说明
roleId int64 角色 ID
permIds []int64 权限 ID 列表(空数组清空绑定)

用户管理

POST /api/user/create — 创建用户(超级管理员 或 产品管理员)

字段 类型 必填 说明
username string 登录名(唯一)
password string 密码(6-72 字符)
nickname string 昵称
email string 邮箱(需合法格式)
phone string 手机号(7-15 位数字,可含 + 前缀)
remark string 备注
deptId int64 部门 ID

响应 data: {"id": 1}

POST /api/user/update — 更新用户(仅本人 或 超级管理员)

支持字段清空:传 "" 清空字符串字段,传 0 清空 deptId,不传字段则不更新。

字段 类型 必填 说明
id int64 用户 ID
nickname *string 昵称
email *string 邮箱
phone *string 手机号
remark *string 备注
deptId *int64 部门 ID(传 0 取消部门)
status int64 状态

POST /api/user/list — 用户列表

字段 类型 必填 说明
productCode string 产品编码(传入则附带成员类型)
page int64 页码
pageSize int64 每页条数

POST /api/user/detail — 用户详情

返回用户信息及绑定的角色 ID 列表(roleIds)。

字段 类型 必填 说明
id int64 用户 ID

POST /api/user/bindRoles — 绑定用户角色(需管理权限)

需通过 CheckManageAccess 权限检查。全量替换该用户的角色。

字段 类型 必填 说明
userId int64 用户 ID
roleIds []int64 角色 ID 列表

POST /api/user/setPerms — 设置用户权限覆盖(需管理权限)

需通过 CheckManageAccess 权限检查。全量替换用户级别的 ALLOW/DENY 权限覆盖。

字段 类型 必填 说明
userId int64 用户 ID
perms array 权限覆盖列表
perms[].permId int64 权限 ID
perms[].effect string ALLOWDENY

POST /api/user/updateStatus — 更新用户状态(需管理权限)

需通过 CheckManageAccess 权限检查。不允许冻结自己和超级管理员。

字段 类型 必填 说明
id int64 用户 ID
status int64 1=正常 2=冻结

产品成员管理(需管理权限)

POST /api/member/add — 添加产品成员

需通过 CheckManageAccess + CheckMemberTypeAssignment 权限检查。不可分配与自己同级或更高级的成员类型。

字段 类型 必填 说明
productCode string 产品编码
userId int64 用户 ID
memberType string ADMIN / DEVELOPER / MEMBER

响应 data: {"id": 1}

POST /api/member/update — 更新成员

需通过 CheckManageAccess + CheckMemberTypeAssignment 权限检查。

字段 类型 必填 说明
id int64 成员记录 ID
memberType string 成员类型
status int64 状态

POST /api/member/remove — 移除成员

需通过 CheckManageAccess 权限检查。级联清理该成员在该产品下的角色绑定和权限覆盖。

字段 类型 必填 说明
id int64 成员记录 ID

POST /api/member/list — 成员列表

字段 类型 必填 说明
productCode string 产品编码
page int64 页码
pageSize int64 每页条数

响应 data: {"total": N, "list": [MemberItem...]}


gRPC 接口文档

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 接口验证)

生产环境部署前,务必修改 AccessSecretRefreshSecretManagementKey 为安全的随机字符串,并确保产品后端的本地验证密钥与 AccessSecret 一致。ManagementKey 仅管理后台前端持有,不可泄露给产品端。


测试

测试概览

指标 数值
测试用例总数 499
已覆盖 TC 498(99.8%)
测试函数 662
测试子用例 744
测试包 23
通过率 99.7%

测试覆盖范围:

  • REST API 测试 — 全部 HTTP 接口的正常路径、参数边界、错误路径
  • gRPC 接口测试 — 全部 5 个 RPC 方法的正常/异常场景
  • 中间件测试 — JWT 鉴权、冻结账号拦截、UserDetails 注入、统一响应
  • 访问控制测试 — access.go 全部函数的正面/负面测试
  • UserDetailsLoader 测试 — 缓存命中/miss、Clean/Del/BatchDel/CleanByProduct、权限计算
  • Logic 层访问控制负面测试 — 非超管/非管理员操作被拒绝
  • Model 层测试 — 9 个 Model 的 CRUD、事务、批量操作、唯一索引方法、自定义方法
  • Logic 层测试 — 业务逻辑单元测试(含 mock 测试)
  • 工具函数测试 — 分页、邮箱/手机校验、JWT 生成解析

测试文档

文件 说明
test-design.md 测试用例设计文档(499 个 TC,含测试场景、输入数据、预期结果)
test-report.md 测试执行报告(含各包耗时、TC 明细、源码审计结果)

运行测试

项目提供 run-test.sh 脚本,自动完成以下流程:

  1. 连通性检查 — 验证 MySQL 和 Redis 是否可达,不可达则提前终止
  2. 编译检查 — 先执行 go build ./...,编译失败则不跑测试
  3. 执行测试 — 运行 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 中扩展自定义查询]

新增 HTTP 接口

# 1. 在 perm.api 中添加类型和路由定义
# 2. 重新生成(已有 logic 文件不会被覆盖)
./gen-api.sh
# 3. 在生成的 logic 文件中填写业务逻辑

新增数据表 Model

# 1. 在 perm.sql 中添加建表语句
# 2. 生成 model(使用自定义模板,包含 *WithTx 事务查询方法)
./gen-model.sh
# 3. 在生成的 sysXxxModel.go 中添加自定义查询方法

自定义 goctl 模板

项目使用自定义模板(cli/goctl/),主要增强:

  • Handler 模板 — 参数解析错误自动包装为 400 Bad Request(而非 500)
  • Model 模板 — 所有 FindOneFindOneByXxx 方法均提供 *WithTx 事务变体,确保事务内查询使用 session 连接