Sem descrição

BaiLuoYan 71ab62bb81 feat: 文档更新 há 1 dia atrás
cli f506783412 fix: 修复返回结构不统一的bug há 4 dias atrás
etc 1d6d298202 perf: 文件上传接口 há 3 dias atrás
internal 1bc0045568 feat: 创建用户时自动生成随机密码, 不明文返回密码, 需通过 /fetchUserCredentials 获取一次性展示的凭证,用户登录之后必须手动修改密码. 新增 /resetPassword 接口,机制与创建用户的机制一样 há 1 dia atrás
pb 228ee889a9 feat: 静态代码审计,修复逻辑bug和安全漏洞 há 4 semanas atrás
permclient 9a4ad6dc05 feat: 静态代码审计,修复逻辑bug和安全漏洞 há 4 semanas atrás
.gitignore 3a7f161433 perf: gitigone há 4 dias atrás
.gitmodules c52b2cbd3f feat: 权限系统代码首次提交 há 1 mês atrás
README.md 71ab62bb81 feat: 文档更新 há 1 dia atrás
audit-report.md fa46acc11e feat: 静态代码审计,修复逻辑bug和安全漏洞 há 3 semanas atrás
gen-api.sh c52b2cbd3f feat: 权限系统代码首次提交 há 1 mês atrás
gen-model.sh fe615b3d0f fix: 修复gen-model.sh中的示例 há 5 dias atrás
go.mod c5fab9a9bc feat: 添加文件上传接口用于存储用户头像 há 4 dias atrás
go.sum c5fab9a9bc feat: 添加文件上传接口用于存储用户头像 há 4 dias atrás
perm.api 1bc0045568 feat: 创建用户时自动生成随机密码, 不明文返回密码, 需通过 /fetchUserCredentials 获取一次性展示的凭证,用户登录之后必须手动修改密码. 新增 /resetPassword 接口,机制与创建用户的机制一样 há 1 dia atrás
perm.go b481ee3d26 feat: 静态代码审计,修复逻辑bug和安全漏洞 há 4 semanas atrás
perm.sql b481ee3d26 feat: 静态代码审计,修复逻辑bug和安全漏洞 há 4 semanas atrás
run-test.sh c52b2cbd3f feat: 权限系统代码首次提交 há 1 mês atrás
test-design.md 1bc0045568 feat: 创建用户时自动生成随机密码, 不明文返回密码, 需通过 /fetchUserCredentials 获取一次性展示的凭证,用户登录之后必须手动修改密码. 新增 /resetPassword 接口,机制与创建用户的机制一样 há 1 dia atrás
test-report.md 1bc0045568 feat: 创建用户时自动生成随机密码, 不明文返回密码, 需通过 /fetchUserCredentials 获取一次性展示的凭证,用户登录之后必须手动修改密码. 新增 /resetPassword 接口,机制与创建用户的机制一样 há 1 dia atrás

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)独立登录接口,超管仅能通过管理后台登录

  • 令牌轮转 — 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           │        │
                              │  └───────────────────────┘        │
                              └────────────────────────────────────┘

关键关系

  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[获取用户权限] --> 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[不添加为成员 = 无权限]

操作权限控制(Access Control)

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

安全架构:四层纵深防护

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

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, productStatus
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 该部门下所有用户的缓存
刷新令牌 Clean(userId) 轮转时递增 tokenVersion 后清除缓存

快速开始

环境要求

  • 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": "客户关系管理", "adminDeptId": 1}

响应中包含 credentialsTicket(一次性凭证票据,5 分钟内有效),请立即用该 ticket 调用 /api/product/fetchInitialCredentials 领取 appKeyappSecretadminUseradminPassword。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": "导出报表"}
  ]
}

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"`
    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 用于令牌吊销:用户注销或刷新令牌时版本递增,旧版本令牌即时失效

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 天。系统采用令牌轮转策略:每次刷新都返回全新的 accessToken 和 refreshToken,旧令牌即时失效(通过递增 tokenVersion 实现)。过期后必须重新登录。

接入检查清单

  • 已获取产品的 appKeyappSecret
  • 产品启动时调用 /api/perm/sync 同步了全部权限
  • 后端配置了与权限系统相同的 AccessSecret
  • 登录接口正确调用权限系统并返回 token
  • JWT 鉴权中间件已加入受保护路由,并验证 tokenType == "access"
  • 业务接口中根据 claims.Perms 做了权限校验
  • 前端在 accessToken 过期时能自动调用 refreshToken 续期(注意:刷新后旧令牌失效,需立即替换)
  • 前端实现注销功能(调用 /api/auth/logout),确保令牌在服务端即时吊销

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/captcha/get — 获取图片验证码

生成一张数字图片验证码,返回验证码 ID 和 Base64 编码的图片数据。仅在 cap.js 未启用时使用,前端应先调用 /api/capjs/endpoint 判断当前验证方式。

字段 类型 必填 说明
width int 图片宽度(像素),默认 240
height int 图片高度(像素),默认 80

响应 data: {"id": "xxx", "base64image": "data:image/png;base64,..."}

POST /api/capjs/endpoint — 获取 cap.js 端点 URL

返回 cap.js 人机验证服务的前端接入地址。前端应在展示登录页前调用此接口,据返回值决定显示图片验证码还是 cap.js 挂件。

无请求参数。

响应 data: {"data": "https://cap.example.com/sitekey/"}{"data": ""} (空串表示 cap.js 未启用,降级为图片验证码)

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

产品终端用户的统一登录入口。用户通过用户名密码 + 产品编码登录指定产品,登录成功后获得 JWT 令牌对和该用户在此产品下的权限列表。

调用场景:

  • 产品前端登录页:用户在 CRM、OA 等业务系统的登录页面输入账号密码,前端调用此接口获取 token
  • 产品后端代理登录:产品后端收到用户凭据后通过 HTTP 或 gRPC 调用此接口,将返回的 token 下发给客户端
  • 移动端 / 小程序登录:移动客户端直接调用此接口完成产品侧身份认证

安全约束:

  • 超级管理员账号无法通过此接口登录(必须走 /auth/adminLogin),防止超管凭据在产品端暴露
  • 产品必须处于启用状态,否则拒绝登录
  • 用户必须是该产品的有效成员(status=1),且账号未冻结
  • 受 IP 维度限流保护,防止暴力破解;仅对已存在的用户名消耗限流配额
  • 验证机制互斥:cap.js 启用时此接口拒绝请求(返回 400),必须走 /auth/login/cap;cap.js 未启用时必须携带图片验证码(captchaId + captchaCode)
字段 类型 必填 说明
username string 登录名
password string 密码
productCode string 产品编码
captchaId string 条件必填 图片验证码 ID(cap.js 未启用时必填)
captchaCode string 条件必填 图片验证码答案(cap.js 未启用时必填)

响应 data

字段 类型 说明
accessToken string 访问令牌(用于后续 API 调用,默认 2 小时有效)
refreshToken string 刷新令牌(用于续期,默认 7 天有效)
expires int64 accessToken 过期时间(Unix 时间戳,秒)
userInfo object 用户信息(含 perms 权限码数组,前端据此控制菜单/按钮显隐)

POST /api/auth/login/cap — 产品端 cap.js 登录

产品端使用 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 相同。

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

权限管理系统自身管理后台的登录入口。仅限超级管理员使用,必须额外提供 managementKey 验证身份。

调用场景:

  • 管理后台前端登录:超级管理员在权限系统管理 UI 的登录页面输入账号密码 + managementKey
  • 运维脚本自动化:运维人员通过脚本调用此接口获取超管 token,执行批量管理操作(如创建产品、批量建用户)

安全约束:

  • 必须传入与服务端配置一致的 managementKey,该密钥仅管理后台前端持有,不可泄露给产品端
  • managementKey 校验在限流之前执行,无效密钥不会消耗限流配额,防止 DoS 攻击
  • 受 IP 维度限流保护
  • 验证机制互斥:cap.js 启用时此接口拒绝请求(返回 400),必须走 /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 中 productCodeperms 为空。超管拥有所有产品的全部权限,不需要在 token 中枚举。

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

管理后台使用 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 相同。

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

使用有效的 refreshToken 换取全新的令牌对。采用令牌轮转策略,每次刷新后旧令牌即时失效,确保单会话安全。

调用场景:

  • accessToken 即将过期时自动续期:前端在 accessToken 到期前(如提前 5 分钟)调用此接口,无感刷新用户会话
  • accessToken 已过期后恢复会话:前端收到 401 响应后,用 refreshToken 尝试换取新令牌,成功则重试原请求,失败则跳转登录
  • 切换产品上下文:管理后台超管登录后,通过传入不同的 productCode 切换到特定产品的权限视角,获取该产品下的权限列表

安全约束:

  • 通过 Authorization: Bearer {refreshToken} 请求头传入,必须是 tokenType=refresh 的令牌
  • 每次刷新递增 tokenVersion,旧的 access/refresh 令牌即时失效——即使令牌被窃取,攻击者也只能用一次
  • 产品禁用时拒绝刷新,立即切断该产品下所有用户的会话
  • refreshToken 有固定有效期(默认 7 天),过期后必须重新登录
字段 类型 必填 说明
Authorization header Bearer {refreshToken}
productCode string 切换产品上下文时传入(Body)

响应 data: 与登录接口相同,包含全新的 accessToken + refreshToken + userInfo。

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

产品后端全量上报权限列表,系统自动对比差异执行新增、更新和禁用操作。这是产品权限声明的唯一入口,管理员无需手动创建权限。

调用场景:

  • 产品服务启动时自动同步:产品后端在 main 函数中调用此接口,确保代码中新增或重命名的权限点自动注册到权限系统。推荐放在服务启动阶段(非请求链路),失败时 log.Fatal 终止启动
  • CI/CD 部署流程中调用:在产品部署脚本中调用此接口,确保新版本的权限定义在服务上线前就已同步
  • 权限定义热更新:产品无需重启即可通过调用此接口更新权限列表(如通过管理接口触发)

处理逻辑:

  • 列表中已存在的权限:按 code 匹配,更新 nameremark
  • 列表中新增的权限:自动创建
  • 数据库中有但列表中没有的权限:自动禁用(status=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}

认证接口(需鉴权)

POST /api/auth/logout — 用户注销

主动吊销当前用户的所有已签发令牌,使其在所有设备/浏览器上立即失效。

调用场景:

  • 用户主动退出登录:前端「退出登录」按钮点击后调用此接口,确保服务端即时吊销令牌(而非仅清理客户端本地存储)
  • 安全事件应急处置:用户发现账号异常(如收到异地登录提醒),通过注销接口使所有设备上的令牌立即失效
  • 管理后台切换账号前:超管在管理后台切换到其他账号前,先注销当前会话

处理逻辑: 递增当前用户的 tokenVersion,所有持有旧版本的 access/refresh 令牌在下次使用时都会被拒绝;同时清除 UserDetailsLoader 缓存。

无请求参数。响应 data: null

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

返回当前登录用户的完整信息,包括基本资料、部门归属、产品成员身份和权限列表。数据来自 UserDetailsLoader 缓存。

调用场景:

  • 前端页面初始化:用户刷新页面或首次加载时,前端调用此接口获取用户信息,渲染头像、昵称、菜单权限等
  • 前端权限刷新:当管理员调整了用户的角色/权限后,前端可主动调用此接口重新获取最新权限列表,无需重新登录
  • 移动端个人中心:展示用户头像、昵称、部门等个人资料
  • 前端权限守卫:路由切换前调用此接口确认用户仍有有效会话及对应权限

无请求参数。响应 data: UserInfo 对象(含 userId、username、nickname、avatar、email、phone、deptName、memberType、perms 等)。

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

用户修改自己的登录密码。修改成功后递增 tokenVersion,所有已签发令牌即时失效,用户需重新登录。

调用场景:

  • 用户主动修改密码:用户在个人设置页面修改密码
  • 首次登录强制改密:管理员创建用户时设置了初始密码,用户首次登录后系统提示修改密码(前端检查 mustChangePassword 标志)
  • 密码泄露后重置:用户怀疑密码泄露,通过修改密码使所有已签发令牌失效

安全约束: 必须验证原密码正确后才能修改;新密码需 6-72 字符且不能与旧密码相同。

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

POST /api/auth/updateInfo — 修改自身信息

已登录用户修改自己的昵称、头像、邮箱、手机号。userId 从 JWT 令牌中提取,不接受客户端传入,防止越权修改他人信息。

调用场景:

  • 个人资料编辑:用户在个人设置页面修改昵称、头像、邮箱或手机号
  • 首次完善资料:管理员创建用户后,用户首次登录补充个人信息
  • 头像更新:用户上传新头像后调用此接口保存头像 URL

安全约束:

  • userId 从 JWT 获取,禁止客户端指定(防 IDOR)
  • 仅允许修改 nickname、avatar、email、phone 四个安全字段
  • 不允许修改 username(登录凭证)、deptId、status、remark 等敏感字段
  • 使用乐观锁(updateTime CAS)防止并发覆盖
  • 至少需要传入一个字段,全部为空时返回 400
字段 类型 必填 说明
nickname string 昵称(上限 64 字符)
avatar string 头像 URL(上限 255 字符)
email string 邮箱(上限 64 字符)
phone string 手机号(上限 32 字符)

响应: 无 data(成功时返回标准 {"code":0,"msg":"ok"} 结构)。若信息被其他会话并发修改,返回 409 冲突错误。

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

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

为一个新的业务系统注册接入权限系统。创建后自动生成 appKey/appSecret(用于权限同步认证)和初始 ADMIN 管理员账号。

调用场景:

  • 新业务系统接入:公司新开发了一个 CRM/OA/电商后台等系统,超管在管理后台创建产品,获取接入凭证交给产品开发团队
  • 多租户场景拓展:SaaS 平台新增一个租户,通过创建产品实现权限隔离

注意事项: 响应中直接返回 appKeyadminUser,但 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"}

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

修改产品的名称、备注或启用/禁用状态。

调用场景:

  • 修改产品信息:产品改名(如从「客户系统」改为「CRM 系统」)、补充备注说明
  • 禁用产品:项目下线或暂停运营时,将产品状态设为禁用(status=2)。禁用后该产品下所有在线用户的令牌即时失效(JWT 中间件和 gRPC 接口均拦截),任何登录/刷新请求都将被拒绝
  • 重新启用产品:暂停后恢复运营,将产品状态改回启用(status=1

副作用: 更新操作会清除该产品下所有用户的 UserDetailsLoader 缓存(CleanByProduct),确保状态变更即时生效。

字段 类型 必填 说明
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 — 产品详情

查询单个产品的完整信息(含 appKey,不含 appSecret)。

调用场景:

  • 产品详情/编辑页面:管理后台进入产品编辑页时,先调用此接口获取当前数据回填表单
  • 查看产品接入凭证:超管需要查看某产品的 appKeyappSecret 仅创建时返回,此接口不包含)
字段 类型 必填 说明
id int64 产品 ID

POST /api/product/fetchInitialCredentials — 获取产品初始凭证

安全地获取产品创建时生成的初始凭证(appKey、appSecret、初始管理员账号密码)。采用一次性 ticket 机制,凭证仅可获取一次。

调用场景:

  • 产品创建后获取凭证:管理后台创建产品成功后,前端使用返回的 ticket 调用此接口获取完整凭证并展示给超管。ticket 使用后立即销毁,无法重复获取

安全约束:

  • 仅超级管理员可调用
  • ticket 存储在 Redis 中,TTL 为 5 分钟,过期后无法获取
  • ticket 采用原子性 GetDel 操作,确保一次性消费,防止重放攻击
字段 类型 必填 说明
ticket string 创建产品时返回的一次性凭证票据

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

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

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

在组织架构中新增一个部门节点。部门是全局概念,不隶属于任何产品。

调用场景:

  • 搭建组织架构:公司初始化时创建一级部门(研发部、市场部、运营部),再创建子部门(前端组、后端组、测试组)
  • 组织调整新增部门:公司扩张新增业务线或团队时,在对应的上级部门下创建新部门
  • 创建研发部门:设置 deptType=DEV 的部门中的成员加入任何产品后自动拥有全部权限,适用于需要跨产品调试的研发团队
字段 类型 必填 说明
parentId int64 父部门 ID,0 表示顶级部门
name string 部门名称
sort int64 排序值(同级部门间的排列顺序,数值越小越靠前)
deptType string NORMAL(默认)或 DEV(研发部门,成员在产品中自动拥有全部权限)
remark string 备注

响应 data: {"id": 1}

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

修改部门名称、排序、类型或启禁用状态。

调用场景:

  • 部门更名:组织调整时修改部门名称(如「技术部」改为「研发中心」)
  • 调整排序:变更部门在同级中的显示顺序
  • 变更部门类型:将普通部门升级为研发部门(NORMALDEV),或将研发部门降级为普通部门。类型变更立即影响该部门下所有用户在各产品中的权限
  • 禁用部门:部门撤销时禁用,保留历史数据

副作用: 更新操作会清除该部门下所有用户的 UserDetailsLoader 缓存,确保部门类型或状态变更即时反映到权限计算中。

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

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

永久删除一个部门。存在子部门或关联用户时拒绝删除,必须先迁移或删除子部门及部门下的用户。

调用场景:

  • 清理空部门:组织架构调整后,将人员迁移到新部门后删除空的旧部门
  • 撤销误创建的部门:部门创建有误时及时删除

约束: 存在子部门或关联用户时返回 400 错误,防止意外删除整个子树。删除前应先通过部门树接口确认该部门无子节点且无用户归属。

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

POST /api/dept/tree — 部门树

返回完整的部门树形结构,包含所有层级的嵌套 childrendeptType 标识。

调用场景:

  • 管理后台组织架构页面:以树形控件展示公司完整的部门层级结构,支持展开/折叠
  • 用户创建/编辑时选择部门:表单中的部门选择器(树形下拉)需要此接口提供数据源
  • 权限检查的辅助参考:管理员查看部门层级关系,了解谁能管理谁(部门层级影响管理权限范围)

无请求参数。

权限管理

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

分页查询指定产品下的所有权限定义。权限由产品后端通过 /perm/sync 自动上报,管理员无法手动创建或删除权限。

调用场景:

  • 角色绑定权限时选择权限:管理后台在为角色分配权限时,需要先展示该产品下所有可用的权限列表(通常以穿梭框或复选列表形式)
  • 用户权限覆盖时选择权限:管理后台在为用户设置 ALLOW/DENY 覆盖时,需要展示权限列表供选择
  • 权限审计:管理员查看某产品当前注册了哪些权限,确认是否与代码中的定义一致
  • 排查权限同步结果:产品部署后确认新增的权限是否已正确同步到系统中
字段 类型 必填 说明
productCode string 产品编码
page int64 页码
pageSize int64 每页条数

响应 data: {"total": N, "list": [PermItem...]}(含 id、code、name、remark、status)

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

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

在指定产品下创建一个新角色。角色是产品级概念,同名角色在不同产品中互不干扰。

调用场景:

  • 按岗位建立角色体系:产品管理员根据业务需求创建角色,如「销售经理」「普通销售」「客服」「财务审批」
  • 按操作范围划分角色:创建「只读角色」「编辑角色」「管理角色」等不同操作级别的角色
  • 新功能上线配套角色:产品新增功能模块时,创建对应的角色(如新增报表模块时创建「报表管理员」角色)

安全约束: 产品必须处于启用状态;permsLevel 数值越小权限越高,用于同级成员间的管理权限比较。

字段 类型 必填 说明
productCode string 所属产品编码
name string 角色名(产品内唯一)
remark string 备注
permsLevel int64 权限等级(数值越小权限越高,用于管理权限判定)

响应 data: {"id": 1}

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

修改角色名称、备注、权限等级或启禁用状态。

调用场景:

  • 调整角色名称和描述:角色职责变化后更新名称和备注
  • 调整权限等级:将角色的 permsLevel 调高或调低,影响该角色用户在管理权限检查中的级别
  • 禁用角色:角色不再使用时禁用(status=2),已绑定该角色的用户在权限计算时将跳过该角色的权限

安全约束: 非超级管理员不可降低角色的 permsLevel(即不可将数值改大),防止产品管理员通过降低角色级别来提升自己的相对权限。

副作用: 更新后自动清除所有绑定该角色的用户在该产品下的 UserDetailsLoader 缓存。

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

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

永久删除一个角色,级联清理该角色的所有权限绑定(sys_role_perm)和用户绑定(sys_user_role)。

调用场景:

  • 清理废弃角色:业务重构后旧角色不再需要,彻底删除以保持角色列表清洁
  • 合并角色:将多个相似角色合并为一个时,先将用户迁移到新角色,再删除旧角色

副作用: 删除前先查询所有绑定该角色的用户 ID 列表,删除后批量清除这些用户在该产品下的缓存,确保权限变更即时生效。

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

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

分页查询指定产品下的所有角色。

调用场景:

  • 管理后台角色管理页面:展示产品下的角色列表,供管理员查看、编辑、删除
  • 用户绑定角色时选择角色:管理后台在为用户分配角色时,需要展示该产品下的角色列表(通常以多选框或穿梭框形式)
  • 权限审计:查看某产品下有哪些角色及其权限等级分布
字段 类型 必填 说明
productCode string 产品编码
page int64 页码
pageSize int64 每页条数

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

查询单个角色的完整信息及其绑定的权限 ID 列表。

调用场景:

  • 角色编辑页面数据回填:管理后台进入角色编辑页时,调用此接口获取角色信息和已绑定的权限列表回填表单
  • 查看角色权限配置:管理员查看某角色具体拥有哪些权限,进行审计或排查
字段 类型 必填 说明
id int64 角色 ID

响应 data: 角色信息 + permIds(该角色绑定的权限 ID 数组)。

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

全量替换该角色绑定的权限列表。传入空数组清空所有绑定。

调用场景:

  • 初始化角色权限:创建角色后为其配置初始权限集合
  • 调整角色权限范围:业务变化时增减角色权限(如销售角色新增「导出报表」权限)
  • 权限收缩:产品下线某功能时,从相关角色中移除对应权限
  • 清空角色权限:传入空 permIds 数组可清除该角色的所有权限绑定

副作用: 操作后自动清除所有绑定该角色的用户在该产品下的缓存,确保权限变更即时反映到在线用户。

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

用户管理

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

创建一个全局用户账号。服务端自动生成符合强度要求的随机密码,通过一次性凭证票据(ticket)安全下发,避免密码明文出现在请求/响应体中。

调用场景:

  • 新员工入职:HR 或管理员为新员工创建账号并归属到对应部门,创建成功后通过 ticket 领取初始密码交给员工
  • 批量建立账号:运维脚本遍历员工花名册,批量调用此接口创建用户账号
  • 外部协作人员:为外包或合作伙伴创建账号(可不设 deptId),后续通过成员管理授予特定产品访问权

注意事项: 创建用户后还需通过 /member/add 将其加入产品才能登录使用该产品。响应中携带 credentialsTicket(5 分钟有效),需立即调用 /api/user/fetchCredentials 领取用户名和初始密码。ticket 一次消费即失效。用户首次登录后会被强制要求修改密码(mustChangePassword=1)。

字段 类型 必填 说明
username string 登录名(全局唯一)
nickname string 昵称
email string 邮箱(需合法格式)
phone string 手机号(7-15 位数字,可含 + 前缀)
remark string 备注
deptId int64 部门 ID(归属研发部门的用户加入产品后自动获得全部权限)

响应 data: {"id": 1, "credentialsTicket": "...", "credentialsExpiresAt": 1716000000}

POST /api/user/resetPassword — 重置用户密码(超级管理员 或 产品管理员)

管理员为指定用户重置密码。服务端生成新的随机强密码,通过一次性凭证票据安全下发。重置后旧令牌即时失效(tokenVersion 递增),用户下次登录需使用新密码且会被强制要求修改密码。

调用场景:

  • 用户忘记密码:用户无法自行修改密码时,管理员为其重置
  • 安全事件处置:发现账号异常时,管理员重置密码使旧凭据失效
  • 新员工初始密码遗失:创建用户时的 ticket 过期或已消费,管理员通过重置密码重新生成

安全约束:

  • 不允许重置超级管理员的密码(超管只能通过 /auth/changePassword 自行修改)
  • 需通过 CheckManageAccess 权限检查(防止越权重置高级别用户)
  • 重置后递增 tokenVersion,所有已签发令牌即时失效
字段 类型 必填 说明
userId int64 目标用户 ID

响应 data: {"credentialsTicket": "...", "credentialsExpiresAt": 1716000000}

POST /api/user/fetchCredentials — 获取用户凭证(超级管理员 或 产品管理员)

安全地获取用户创建或密码重置时生成的凭证(用户名 + 密码)。采用一次性 ticket 机制,凭证仅可获取一次。

调用场景:

  • 创建用户后获取凭证:管理后台创建用户成功后,前端使用返回的 ticket 调用此接口获取初始密码并展示给管理员
  • 重置密码后获取凭证:管理员重置用户密码后,前端使用返回的 ticket 获取新密码

安全约束:

  • 仅超级管理员或产品管理员可调用
  • ticket 存储在 Redis 中,TTL 为 5 分钟,过期后无法获取
  • ticket 采用原子性 GetDel 操作,确保一次性消费,防止重放攻击
字段 类型 必填 说明
ticket string 创建用户或重置密码时返回的一次性凭证票据

响应 data: {"username": "zhangsan", "password": "Ax7#kL9m..."}

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

修改用户个人信息(昵称、邮箱、手机、部门等)。使用乐观锁防止并发更新冲突。

调用场景:

  • 用户修改个人资料:用户在个人设置页面更新昵称、邮箱、手机号
  • 超管调整用户部门:组织架构调整时,超管将用户从一个部门转移到另一个部门(修改 deptId)。部门变更会影响权限——从研发部门转出的用户将失去自动全权限特权
  • 超管补充用户信息:为缺失信息的用户补充邮箱、手机等联系方式
  • 取消部门归属:传 deptId=0 将用户从部门中移出

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

副作用: 更新后清除该用户所有产品的 UserDetailsLoader 缓存。

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

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

分页查询用户列表,支持按用户名、昵称、状态、部门筛选,可选传入产品编码以附带成员身份信息。

调用场景:

  • 管理后台用户管理页面:展示所有用户列表,含基本信息和状态,支持多条件组合筛选
  • 按产品筛选成员视角:传入 productCode 时,列表中每个用户会附带其在该产品中的 memberType(ADMIN/DEVELOPER/MEMBER/空),方便管理员了解哪些用户已经是该产品的成员
  • 添加成员时选择用户:管理后台在添加产品成员时,先查询用户列表让管理员选择要添加的用户
字段 类型 必填 说明
productCode string 产品编码(传入则每个用户附带在该产品中的成员类型)
username string 用户名(模糊匹配)
nickname string 昵称(模糊匹配)
status int64 状态筛选(1=启用,2=禁用)
deptId *int64 部门 ID(0 表示无部门)
page int64 页码
pageSize int64 每页条数

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

查询单个用户的完整信息及其在当前产品下绑定的角色 ID 列表。

调用场景:

  • 用户编辑页面数据回填:管理后台进入用户编辑页时,调用此接口获取用户信息回填表单
  • 查看用户角色配置:管理员查看某用户当前绑定了哪些角色,评估权限是否合理
  • 用户权限排查:当用户反馈无法访问某功能时,管理员通过此接口查看用户的角色绑定情况
字段 类型 必填 说明
id int64 用户 ID
productCode string 产品编码;仅 SUPER_ADMIN 有效,传入时返回该用户在指定产品下绑定的角色 ID;不传时返回该用户在所有产品下的全量角色 ID。非超管始终使用 JWT 中的产品上下文

响应 data: 用户信息 + roleIds(超管不传 productCode 时为跨所有产品的角色 ID;否则为指定产品下的角色 ID)。

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

全量替换该用户在当前产品下的角色绑定。需通过 CheckManageAccess 权限检查。

调用场景:

  • 为新成员分配角色:用户被添加为产品成员后,管理员为其分配一个或多个角色
  • 调整用户职责:用户岗位变动时,更换其角色(如从「普通销售」升级为「销售经理」)
  • 多角色组合:为用户同时分配多个角色以组合权限(如同时拥有「订单管理」和「报表查看」角色)
  • 清空角色:传入空 roleIds 数组清除用户的所有角色绑定

安全约束: 操作者需要对目标用户有管理权限(通过 CheckManageAccess 校验超管/部门层级/权限级别);目标用户的成员状态必须启用。

副作用: 操作后清除目标用户在该产品下的 UserDetailsLoader 缓存。

字段 类型 必填 说明
userId int64 用户 ID
roleIds []int64 角色 ID 列表(全量替换)
productCode string 条件必填 产品编码;仅 SUPER_ADMIN 有效,超管必须显式传入(为空时返回 400),非超管忽略此字段始终使用 JWT 中的产品上下文

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

全量替换用户在当前产品下的个性化 ALLOW/DENY 权限覆盖。这是在角色权限基础上的微调机制。需通过 CheckManageAccess 权限检查。

调用场景:

  • 给个别用户额外授权:用户的角色不包含某权限,但业务上需要临时或长期授予(如给李四额外授予「导出报表」权限),使用 ALLOW
  • 给个别用户限制权限:用户的角色包含某权限,但需要对该用户个别禁止(如禁止实习生「删除客户」),使用 DENY
  • 组合 ALLOW 和 DENY:同时为用户增加某些权限并限制某些权限,实现精细化控制
  • 清空所有覆盖:传入空 perms 数组清除所有个性化覆盖,用户权限完全由角色决定

权限计算: 最终权限 = (角色权限 ∪ ALLOW) - DENY。DENY 优先级最高。

安全约束: 目标用户的成员状态必须启用;产品必须处于启用状态。

字段 类型 必填 说明
userId int64 用户 ID
perms array 权限覆盖列表(全量替换,空数组清除所有覆盖)
perms[].permId int64 权限 ID
perms[].effect string ALLOW(额外授予)或 DENY(强制拒绝)

POST /api/user/userPerms — 查看用户权限覆盖

查询指定用户在当前产品下已配置的个性化 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。

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

冻结或解冻用户账号。冻结后用户立即无法访问任何产品。需通过 CheckManageAccess 权限检查。

调用场景:

  • 员工离职冻结:员工离职后冻结其账号,即时切断所有产品的访问权限(无需逐个产品移除成员)
  • 安全事件处置:发现账号被盗用或存在违规行为时,紧急冻结账号
  • 临时停权:员工休长假或处于考察期时临时冻结
  • 解冻恢复:冻结原因消除后恢复账号,用户无需重新配置角色和权限

安全约束: 不允许冻结自己;不允许冻结超级管理员;需要对目标用户有管理权限。

副作用: 状态变更后递增该用户的 tokenVersion(使所有已签发令牌失效)并清除缓存。

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

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

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

将一个已有用户添加为指定产品的成员,并指定成员类型。用户成为产品成员后才能登录该产品。

调用场景:

  • 新员工加入产品:创建用户后,将其添加为 CRM/OA 等产品的成员,使其可以登录使用
  • 研发人员跨产品参与:研发人员需要参与另一个产品的开发,将其添加为该产品的 MEMBER(因为归属研发部门,自动获得全部权限)
  • 提升为产品管理员:将用户添加为 ADMIN 类型,使其拥有该产品下的角色/用户管理权限
  • 设置产品开发者:将用户添加为 DEVELOPER 类型,使其自动拥有该产品的全部功能权限但不具备管理权限

安全约束:

  • 产品必须处于启用状态
  • 操作者不可分配与自己同级或更高级的成员类型(如 ADMIN 不能将他人设为 ADMIN)
  • 需通过 CheckManageAccess + CheckMemberTypeAssignment 权限检查
字段 类型 必填 说明
productCode string 产品编码
userId int64 用户 ID
memberType string ADMIN(产品管理员)/ DEVELOPER(开发者,全权限)/ MEMBER(普通成员,按角色授权)

响应 data: {"id": 1}

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

修改成员类型或启禁用成员状态。

调用场景:

  • 成员升级:将普通 MEMBER 提升为 DEVELOPER 或 ADMIN(如项目负责人变动)
  • 成员降级:将 ADMIN 或 DEVELOPER 降级为 MEMBER(如不再负责管理工作)
  • 禁用成员:临时禁止某成员访问产品,但保留成员关系和角色绑定(恢复时权限不变)
  • 恢复成员:将禁用的成员重新启用

安全约束:

  • 不可降级产品最后一个活跃 ADMIN——系统保证每个产品至少有一个 ADMIN,防止产品进入无管理状态
  • 操作者不可将成员提升到与自己同级或更高的类型
  • 需通过 CheckManageAccess + CheckMemberTypeAssignment 权限检查

副作用: 更新后清除该用户在该产品下的 UserDetailsLoader 缓存。若操作属于"降权"(ADMIN/DEVELOPERMEMBER,或启用 → 禁用),还会递增该用户的 tokenVersion 并失效底层用户缓存,使其已签发的令牌在下次请求时被强制踢出,须重新登录。升权路径(MEMBERADMIN 等)不触发强制重登。

字段 类型 必填 说明
id int64 成员记录 ID
memberType string 成员类型(不传则不修改)
status int64 状态(不传则不修改);两字段均不传时返回 400

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

将成员从产品中移除。移除后用户无法再登录该产品,其在该产品下的所有角色绑定和权限覆盖被级联清理。

调用场景:

  • 员工不再参与某产品:研发人员调岗不再负责 CRM 项目,从 CRM 产品成员中移除
  • 外部协作结束:外包人员合同到期,从产品中移除其成员资格
  • 权限收紧:安全审计后移除不应有权限的人员

安全约束:

  • 不可移除产品最后一个活跃 ADMIN——防止产品进入无管理状态
  • 需通过 CheckManageAccess 权限检查

级联清理: 在事务内同时清理 sys_user_role(该用户在该产品下的角色绑定)和 sys_user_perm(该用户在该产品下的权限覆盖),然后删除成员记录并清除缓存。

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

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

分页查询指定产品下的所有成员,含用户基本信息和成员类型/状态。

调用场景:

  • 管理后台成员管理页面:展示产品下的所有成员列表,供管理员查看、编辑、移除
  • 成员权限概览:管理员查看产品有哪些成员、各自是什么类型(ADMIN/DEVELOPER/MEMBER),评估权限分配是否合理
  • 成员数量统计:了解产品的使用人数和成员构成
字段 类型 必填 说明
productCode string 产品编码
page int64 页码
pageSize int64 每页条数

响应 data: {"total": N, "list": [MemberItem...]}(含 userId、username、nickname、memberType、status 等)

POST /api/member/userProducts — 查询用户加入的产品列表

查询指定用户作为成员加入的所有产品,含成员类型和状态。

调用场景:

  • 管理后台分配角色/权限前选择产品:超管在「分配角色」或「设置权限」抽屉中,需要先选择产品,此接口提供该用户实际加入的产品列表,避免选择无效产品

访问控制:

  • 超级管理员:可查任意用户
  • 本人:可查自己
  • 其他已登录用户:返回 403,防止枚举他人产品归属(IDOR)
字段 类型 必填 说明
userId int64 目标用户 ID

响应 data: {"list": [{"productCode", "productName", "memberType", "status"}]}

文件上传(需鉴权)

POST /api/minio/upload — 上传文件

将文件上传到 MinIO 对象存储,返回访问 URL 和文件路径。支持按 fileType 分类存储到不同目录,自动去重(基于 MD5)。

调用场景:

  • 用户头像上传:用户在个人设置页面上传头像图片,fileTypeavatar
  • 其他业务文件:根据配置的 fileType 上传不同类型的文件到对应目录

请求格式: multipart/form-data(非 JSON),最大 32MB

字段 类型 必填 说明
file file 上传的文件
fileType string 文件类型标识(需在服务端 Minio 配置中预定义,如 avatar

安全约束:

  • fileType 必须在服务端配置的允许列表中,未配置的类型拒绝上传
  • 可按 fileType 配置允许的 Content-Type 白名单(如头像仅允许 image/jpegimage/png
  • 基于 MD5 去重:相同文件不会重复存储,直接返回已有文件的信息

响应 data: {"url": "https://...", "path": "avatar/2026/05/13/xxx.png", "md5": "...", "size": 102400}


gRPC 接口文档

gRPC 服务定义见 pb/perm.proto,默认监听 :10002。所有 gRPC 错误使用标准 status.Error(codes.Xxx, msg) 格式。

gRPC 接口主要面向产品后端服务间调用,相比 HTTP 接口具有更高性能和更强的类型安全。推荐 Go 项目通过 permclient 包直接调用。

SyncPermissions — 同步权限声明

产品后端在启动阶段全量上报权限列表,与 HTTP 的 /perm/sync 功能等价。

调用场景:

  • Go 微服务启动时自动同步:在 main 函数中通过 gRPC 调用同步权限,相比 HTTP 调用减少序列化开销
  • 服务网格内部调用:在 Kubernetes 等容器编排环境中,服务间直接通过 gRPC 通信,无需经过 HTTP 网关

认证方式: 通过 appKey/appSecret 认证(与 HTTP 接口一致)。

Login — 产品端登录

产品后端代理终端用户进行身份认证,返回 JWT 令牌对和权限列表。

调用场景:

  • Go 后端代理登录:产品的 Go 后端收到用户登录请求后,通过 gRPC 调用权限系统进行身份验证,将返回的令牌下发给前端。相比 HTTP 调用延迟更低
  • 服务间认证委托:微服务架构中,网关服务通过 gRPC 调用权限系统完成用户认证

安全约束: productCode 必传;超级管理员被拒绝(必须走 adminLogin)。

RefreshToken — 刷新令牌(轮转)

使用 refreshToken 换取全新令牌对,采用令牌轮转策略。

调用场景:

  • Go 后端代理续期:产品后端代替前端执行令牌刷新(如 BFF 模式下,后端持有 refreshToken 并管理令牌生命周期)
  • 长连接会话保活:WebSocket 或长连接服务在 accessToken 即将过期时,通过 gRPC 刷新令牌维持会话

安全约束: 每次刷新递增 tokenVersion,旧令牌即时失效;产品禁用时返回 codes.PermissionDenied

VerifyToken — 验证令牌

服务端验证 accessToken 的有效性,返回用户信息和权限列表。

调用场景:

  • 不支持本地 JWT 验证的语言/框架:非 Go 语言的产品后端(如 Python、Node.js)可能未配置 JWT 密钥,通过 gRPC 调用权限系统进行服务端验证
  • 需要实时权限的场景:本地 JWT 验证只能拿到签发时的权限快照;如果需要实时反映权限变更(如管理员刚修改了用户角色),可通过此接口获取最新权限
  • 关键操作的二次验证:对敏感操作(如资金操作、数据删除),在本地 JWT 验证基础上再通过此接口做一次服务端校验,确保令牌未被吊销

推荐实践: 常规场景推荐本地 JWT 验证(零网络开销);仅在上述特殊场景使用此接口。

安全约束: 产品禁用时返回 codes.PermissionDenied

GetUserPerms — 获取用户权限

实时查询指定用户在指定产品下的最新权限码列表。

调用场景:

  • 实时权限网关:API 网关在每次请求时通过 gRPC 查询用户最新权限,实现实时权限控制(适用于权限变更需要立即生效的高安全场景)
  • 后台任务权限校验:异步任务/定时任务执行时,需要校验发起用户是否仍有执行权限
  • 权限缓存刷新:产品后端自行维护权限缓存,定期通过此接口刷新,确保与权限系统保持一致
  • 管理后台权限预览:管理员查看某用户在某产品下的实时权限,用于排查和审计

Go SDK 使用示例

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 验证使用,不可暴露给前端)

生产环境部署前,务必修改 AccessSecretRefreshSecretManagementKey 为安全的随机字符串,并确保产品后端的本地验证密钥与 AccessSecret 一致。ManagementKey 仅管理后台前端持有,不可泄露给产品端。启用 cap.js 时,Capjs.Secret 同样属于服务端私密配置,不得暴露给前端。

反向代理部署

当服务部署在 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 直连后端
  • gRPC 接口不受此配置影响,始终通过 TCP 对端地址获取 IP

测试

测试概览

指标 数值
测试用例总数 1065
已覆盖 TC 1065(100%)
测试函数 1170
测试子用例 1309
测试包 28
通过率 100%

测试覆盖范围:

  • 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 测试用例设计文档(1065 个 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 连接