# 统一权限管理系统(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 时自动拒绝,防止产品进入无管理状态 ## 系统架构 ```mermaid 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 ``` --- ## 核心概念与实体关系 ### 实体关系总览 ```mermaid 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` 自动上报 | 产品级 | ### 实体间关系详解 ```text 全局组织架构 产品权限体系(按产品隔离) ┌─────────────────┐ ┌────────────────────────────────────┐ │ 部门 (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 | 角色绑定 + 用户级覆盖计算 | ### 权限计算流程 ```mermaid 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 系统**、**电商后台**。研发人员需要在产品间流动。 ### 第一步:搭建组织架构 ```mermaid graph TD ROOT[公司] --> DEV[研发部 deptType=DEV] ROOT --> MKT[市场部 deptType=NORMAL] ROOT --> OPS[运营部 deptType=NORMAL] DEV --> FE[前端组] DEV --> BE[后端组] DEV --> QA[测试组] ``` ```bash # 创建顶级部门 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` 的部门中的成员,只要被添加到某个产品下,就自动拥有该产品的全部权限。当从产品成员中移除时,权限立即收回。 ### 第二步:注册产品 ```bash 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 为例: ```json { "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": "导出报表"} ] } ``` 权限由产品代码自动管理,管理员无需手动创建。 ### 第四步:创建角色并绑定权限 ```bash # 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 的开发 ```bash # 创建用户,归属研发部 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** — 只需移除成员关系 ```bash POST /api/member/remove {"id": <张三在OA中的成员记录ID>} # OA 的权限立即收回,CRM 的权限不受影响 ``` **场景 C:市场部李四** — 需要使用 CRM,分配销售角色 ```bash 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], "productCode": "crm"} # 分配"普通销售"角色 ``` 李四登录 CRM 后拥有的权限 = 普通销售角色的权限 = `[customer:list, customer:create, order:list, order:create]`。 **场景 D:给李四额外权限** — 在角色基础上微调 ```bash # 额外给李四"导出报表"权限,但禁止他"创建订单" 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:外部临时协作人员** — 不属于任何部门 ```bash 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], "productCode": "crm"} # 分配"客服"角色(只读) ``` ### 权限配置决策树 ```mermaid flowchart TD Q1{该用户是研发人员?} Q1 -->|是| A1[归属研发部门 deptType=DEV] A1 --> A2["添加为产品成员(MEMBER)即可
自动拥有全部权限"] Q1 -->|否| Q2{该用户是产品管理员?} Q2 -->|是| A3["添加为产品成员(ADMIN)
自动拥有全部权限"] Q2 -->|否| Q3{需要精细权限控制?} Q3 -->|是| A4["添加为产品成员(MEMBER)"] A4 --> A5[分配角色 + 可选 ALLOW/DENY 覆盖] Q3 -->|否| A6[不添加为成员 = 无权限] ``` --- ## 操作权限控制(Access Control) 系统内置一套集中式操作权限管控机制,对所有管理类接口实施多维度的访问控制,防止越权操作。 ### 安全架构:四层纵深防护 ```mermaid flowchart LR REQ[HTTP 请求] --> L1["① JWT 中间件
解析 token / 验证签名"] L1 --> L2["② UserDetailsLoader
加载用户实时状态"] L2 --> L3["③ 多维状态检查
用户/产品/成员状态"] L3 --> L4["④ Logic 层
操作权限控制"] L4 --> BIZ[业务逻辑] ``` | 层级 | 组件 | 职责 | | ------ | ------ | ------ | | 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType`、校验 `tokenVersion` | | 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限、产品状态) | | 第三层 | 多维状态检查 | 用户冻结(`status ≠ 1`)→ 403;产品禁用(`productStatus ≠ 1`)→ 403;成员无效(`memberType` 为空)→ 403 | | 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 | ### 接口操作权限矩阵 | 接口 | 权限要求 | 额外检查 | | ------ | ---------- | ---------- | | **产品管理** | | | | 创建产品 | 仅超级管理员 | — | | 更新产品 | 仅超级管理员 | — | | **部门管理** | | | | 创建部门 | 仅超级管理员 | — | | 更新部门 | 仅超级管理员 | — | | 删除部门 | 仅超级管理员 | 有子部门时拒绝 | | **角色管理** | | | | 创建角色 | 超管 或 产品管理员 | 产品必须启用 | | 更新角色 | 超管 或 产品管理员 | 非超管不可降低 permsLevel | | 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 | | 绑定角色权限 | 超管 或 产品管理员 | — | | **用户管理** | | | | 创建用户 | 超管 或 产品管理员 | — | | 更新用户信息 | 仅本人 或 超管 | — | | 冻结/解冻用户 | 通过 `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` 是管理类操作的核心权限检查函数,执行多维度判定: ```mermaid 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{目标在操作者
本部门或子部门?} 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+ ### 部署步骤 ```bash # 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 ``` ### 初始化超级管理员 首次部署后,需手动在数据库中插入超级管理员账号: ```sql INSERT INTO sys_user (username, password, nickname, isSuperAdmin, status, createTime, updateTime) VALUES ('superadmin', '$2a$10$这里替换为bcrypt加密后的密码', '超级管理员', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); ``` 可使用任意 bcrypt 工具生成密码哈希,或在 Go 中执行: ```go hash, _ := bcrypt.GenerateFromPassword([]byte("your-password"), bcrypt.DefaultCost) fmt.Println(string(hash)) ``` --- ## 产品接入指南 本节以 "CRM 系统"(编码 `crm`)为例,完整演示接入流程。 ### 接入全景图 ```mermaid 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 ``` ### 阶段一:注册产品 超级管理员登录后创建产品: ```bash POST /api/product/create {"code": "crm", "name": "CRM 系统", "remark": "客户关系管理", "adminDeptId": 1} ``` 响应中包含 `credentialsTicket`(一次性凭证票据,5 分钟内有效),请立即用该 ticket 调用 `/api/product/fetchInitialCredentials` 领取 `appKey`、`appSecret`、`adminUser`、`adminPassword`。ticket 一次性消费,过期或已使用后无法重新获取。 ### 阶段二:产品启动时同步权限 CRM 后端在启动阶段上报全部权限列表。**此接口无需 JWT,通过 appKey + appSecret 认证**。 ```bash 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 启动流程中) ```go 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 ```bash POST /api/auth/login {"username": "zhangsan", "password": "123456", "productCode": "crm"} ``` ##### 方式 B:gRPC SDK(推荐 Go 项目) ```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 即可获取用户信息和权限列表,无需每次回调。 ```go 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. 业务接口中检查权限 ```go func CreateOrderHandler(w http.ResponseWriter, r *http.Request) { claims := middleware.GetClaims(r.Context()) if !hasPermission(claims.Perms, "order:create") { // 返回 403 return } // 执行业务逻辑... } ``` #### 4. Token 续期 ```bash POST /api/auth/refreshToken Authorization: Bearer eyJhbGciOi... # refreshToken 放在 header 中 Content-Type: application/json {"productCode": "crm"} ``` > `refreshToken` 有效期默认 7 天。系统采用令牌轮转策略:每次刷新都返回全新的 accessToken 和 refreshToken,旧令牌即时失效(通过递增 `tokenVersion` 实现)。过期后必须重新登录。 ### 接入检查清单 - [ ] 已获取产品的 `appKey` 和 `appSecret` - [ ] 产品启动时调用 `/api/perm/sync` 同步了全部权限 - [ ] 后端配置了与权限系统相同的 `AccessSecret` - [ ] 登录接口正确调用权限系统并返回 token - [ ] JWT 鉴权中间件已加入受保护路由,并验证 `tokenType == "access"` - [ ] 业务接口中根据 `claims.Perms` 做了权限校验 - [ ] 前端在 `accessToken` 过期时能自动调用 `refreshToken` 续期(注意:刷新后旧令牌失效,需立即替换) - [ ] 前端实现注销功能(调用 `/api/auth/logout`),确保令牌在服务端即时吊销 --- ## HTTP API 接口文档 所有接口基础路径为 `/api`,请求方式统一为 **POST**,参数通过 **JSON Body** 传递。需鉴权接口须携带 `Authorization: Bearer {accessToken}`。 ### 统一响应格式 所有 HTTP 接口均返回统一的 JSON 结构: ```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 中 `productCode` 和 `perms` 为空。超管拥有所有产品的全部权限,不需要在 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` 匹配,更新 `name` 和 `remark` - 列表中新增的权限:自动创建 - 数据库中有但列表中没有的权限:自动禁用(`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 平台新增一个租户,通过创建产品实现权限隔离 **注意事项:** 响应中直接返回 `appKey` 和 `adminUser`,但 `appSecret` 与初始 `adminPassword` 不在响应体中——响应携带一次性 `credentialsTicket`(5 分钟有效),需立即调用 `/api/product/fetchInitialCredentials` 领取完整凭证。ticket 一次消费即失效,过期或已使用后无法重新获取。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | code | string | 是 | 产品编码(全局唯一,仅允许字母/数字/下划线/中划线,不能以数字开头,上限 64 字符) | | name | string | 是 | 产品名称 | | remark | string | 否 | 备注 | | adminDeptId | int64 | 是 | 初始管理员账号所属部门 ID(归属研发部门的管理员将自动拥有全部权限) | **响应 data:** `{"id", "code", "appKey", "adminUser", "credentialsTicket", "credentialsExpiresAt"}` #### 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`)。 **调用场景:** - **产品详情/编辑页面**:管理后台进入产品编辑页时,先调用此接口获取当前数据回填表单 - **查看产品接入凭证**:超管需要查看某产品的 `appKey`(`appSecret` 仅创建时返回,此接口不包含) | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | 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 — 更新部门 修改部门名称、排序、类型或启禁用状态。 **调用场景:** - **部门更名**:组织调整时修改部门名称(如「技术部」改为「研发中心」) - **调整排序**:变更部门在同级中的显示顺序 - **变更部门类型**:将普通部门升级为研发部门(`NORMAL` → `DEV`),或将研发部门降级为普通部门。类型变更立即影响该部门下所有用户在各产品中的权限 - **禁用部门**:部门撤销时禁用,保留历史数据 **副作用:** 更新操作会清除该部门下所有用户的 UserDetailsLoader 缓存,确保部门类型或状态变更即时反映到权限计算中。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | id | int64 | 是 | 部门 ID | | name | string | 是 | 名称 | | sort | int64 | 否 | 排序值 | | deptType | string | 否 | `NORMAL` 或 `DEV` | | remark | string | 否 | 备注 | | status | int64 | 否 | 状态 | #### POST /api/dept/delete — 删除部门 永久删除一个部门。存在子部门或关联用户时拒绝删除,必须先迁移或删除子部门及部门下的用户。 **调用场景:** - **清理空部门**:组织架构调整后,将人员迁移到新部门后删除空的旧部门 - **撤销误创建的部门**:部门创建有误时及时删除 **约束:** 存在子部门或关联用户时返回 400 错误,防止意外删除整个子树。删除前应先通过部门树接口确认该部门无子节点且无用户归属。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | id | int64 | 是 | 部门 ID | #### POST /api/dept/tree — 部门树 返回完整的部门树形结构,包含所有层级的嵌套 `children` 和 `deptType` 标识。 **调用场景:** - **管理后台组织架构页面**:以树形控件展示公司完整的部门层级结构,支持展开/折叠 - **用户创建/编辑时选择部门**:表单中的部门选择器(树形下拉)需要此接口提供数据源 - **权限检查的辅助参考**:管理员查看部门层级关系,了解谁能管理谁(部门层级影响管理权限范围) 无请求参数。 ### 权限管理 #### 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 — 创建用户(超级管理员 或 产品管理员) 创建一个全局用户账号。用户是全局概念,创建后可通过「添加成员」接口加入多个产品。 **调用场景:** - **新员工入职**:HR 或管理员为新员工创建账号,设置初始密码,并归属到对应部门 - **批量建立账号**:运维脚本遍历员工花名册,批量调用此接口创建用户账号 - **外部协作人员**:为外包或合作伙伴创建账号(可不设 `deptId`),后续通过成员管理授予特定产品访问权 **注意事项:** 创建用户后还需通过 `/member/add` 将其加入产品才能登录使用该产品。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | 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 — 更新用户(仅本人 或 超级管理员) 修改用户个人信息(昵称、邮箱、手机、部门等)。使用乐观锁防止并发更新冲突。 **调用场景:** - **用户修改个人资料**:用户在个人设置页面更新昵称、邮箱、手机号 - **超管调整用户部门**:组织架构调整时,超管将用户从一个部门转移到另一个部门(修改 `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 | 否 | 产品编码(传入则每个用户附带在该产品中的成员类型) | | 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`/`DEVELOPER` → `MEMBER`,或启用 → 禁用),还会递增该用户的 `tokenVersion` 并失效底层用户缓存,使其已签发的令牌在下次请求时被强制踢出,须重新登录。升权路径(`MEMBER` → `ADMIN` 等)不触发强制重登。 | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | 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)。 **调用场景:** - **用户头像上传**:用户在个人设置页面上传头像图片,`fileType` 传 `avatar` - **其他业务文件**:根据配置的 `fileType` 上传不同类型的文件到对应目录 **请求格式:** `multipart/form-data`(非 JSON),最大 32MB | 字段 | 类型 | 必填 | 说明 | | ------ | ------ | ------ | ------ | | file | file | 是 | 上传的文件 | | fileType | string | 是 | 文件类型标识(需在服务端 Minio 配置中预定义,如 `avatar`) | **安全约束:** - `fileType` 必须在服务端配置的允许列表中,未配置的类型拒绝上传 - 可按 `fileType` 配置允许的 Content-Type 白名单(如头像仅允许 `image/jpeg`、`image/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 使用示例 ```go 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{...}) ``` --- ## 项目结构 ```text 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/tokenVersion 验证 + 用户/产品/成员状态检查 + UserDetails 注入) ├── svc/serviceContext.go # 依赖注入容器 ├── server/permserver.go # gRPC 服务实现 ├── types/types.go # 请求/响应结构体(goctl 生成) ├── handler/ # HTTP handler(按模块分组,goctl 生成) │ ├── routes.go │ ├── pub/ # 公开接口(登录/刷新令牌/同步权限) │ ├── auth/ # 认证接口(用户信息/修改密码/注销) │ ├── product/ # 产品管理 │ ├── dept/ # 部门管理 │ ├── perm/ # 权限管理 │ ├── role/ # 角色管理 │ ├── user/ # 用户管理 │ └── member/ # 产品成员 ├── logic/ # 业务逻辑层(按模块分组) │ ├── auth/ │ │ ├── jwt.go # JWT 生成/解析(含 tokenType 区分) │ │ ├── perms.go # 权限计算核心逻辑 │ │ └── access.go # 集中式操作权限控制(超管/管理员/部门/级别检查) │ ├── pub/ # 登录、刷新、同步 │ ├── product/ # 产品 CRUD │ ├── dept/ # 部门 CRUD + 树 │ ├── perm/ # 权限列表 │ ├── role/ # 角色 CRUD + 绑定权限 + 级联删除 │ ├── user/ # 用户 CRUD + 绑定角色/权限 │ └── member/ # 成员管理 + 级联清理 └── model/ # 数据模型层(每张表独立目录) ├── models.go # Models 聚合结构体(统一初始化所有 Model 实例) ├── product/ # sys_product ├── dept/ # sys_dept ├── perm/ # sys_perm ├── role/ # sys_role ├── roleperm/ # sys_role_perm ├── user/ # sys_user ├── userperm/ # sys_user_perm ├── userrole/ # sys_user_role └── productmember/ # sys_product_member ``` --- ## 配置说明 配置文件位于 `etc/perm-api.yaml`: | 配置项 | 说明 | 默认值 | | -------- | ------ | -------- | | `Host` / `Port` | HTTP 监听地址 | `0.0.0.0:10001` | | `RpcServerConf.ListenOn` | gRPC 监听地址 | `0.0.0.0:10002` | | `MySQL.DataSource` | MySQL 连接串 | — | | `CacheRedis[].Host` | Redis 地址 | `127.0.0.1:6379` | | `Auth.AccessSecret` | JWT accessToken 签名密钥 | — | | `Auth.AccessExpire` | accessToken 有效期(秒) | `7200`(2h) | | `Auth.RefreshSecret` | JWT refreshToken 签名密钥 | — | | `Auth.RefreshExpire` | refreshToken 有效期(秒) | `604800`(7d) | | `Auth.ManagementKey` | 管理后台登录密钥(`/auth/adminLogin` 接口验证) | — | | `BehindProxy` | 是否部署在反向代理(Nginx 等)后面 | `false` | | `Capjs.Enable` | cap.js 人机验证开关(`1` 启用,`0` 或不配置则使用图片验证码) | `0` | | `Capjs.EndpointURL` | cap.js 服务地址(如 `https://cap.example.com`) | — | | `Capjs.Key` | cap.js site key(前端挂件加载和 siteverify 路径使用) | — | | `Capjs.Secret` | cap.js site secret(服务端 siteverify 验证使用,不可暴露给前端) | — | > **生产环境部署前,务必修改 `AccessSecret`、`RefreshSecret` 和 `ManagementKey` 为安全的随机字符串,并确保产品后端的本地验证密钥与 `AccessSecret` 一致。`ManagementKey` 仅管理后台前端持有,不可泄露给产品端。启用 cap.js 时,`Capjs.Secret` 同样属于服务端私密配置,不得暴露给前端。** ### 反向代理部署 当服务部署在 Nginx 等反向代理后面时,需要开启 `BehindProxy` 配置以正确获取客户端真实 IP(用于登录限流等安全策略): #### 1. 配置文件设置 ```yaml BehindProxy: true ``` #### 2. Nginx 配置要求 必须在 Nginx 中正确设置 `X-Real-IP` 并清除客户端可伪造的 `X-Forwarded-For`: ```nginx 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 --- ## 测试 ### 测试概览 | 指标 | 数值 | | ------ | ------ | | 测试用例总数 | 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`,结束后汇总通过/失败的包数和耗时 ```bash # 运行全部测试 ./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` | 每个测试运行次数 | ```bash # 组合使用示例 TEST_VERBOSE=1 TEST_RUN="TestLogin" ./run-test.sh ./internal/logic/pub/... ``` ### 测试文件组织 测试文件遵循 Go 标准命名,与被测文件同目录: ```text 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 标准开发流程: ```mermaid 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 接口 ```bash # 1. 在 perm.api 中添加类型和路由定义 # 2. 重新生成(已有 logic 文件不会被覆盖) ./gen-api.sh # 3. 在生成的 logic 文件中填写业务逻辑 ``` ### 新增数据表 Model ```bash # 1. 在 perm.sql 中添加建表语句 # 2. 生成 model(使用自定义模板,包含 *WithTx 事务查询方法) ./gen-model.sh # 3. 在生成的 sysXxxModel.go 中添加自定义查询方法 ``` ### 自定义 goctl 模板 项目使用自定义模板(`cli/goctl/`),主要增强: - **Handler 模板** — 参数解析错误自动包装为 `400 Bad Request`(而非 500) - **Model 模板** — 所有 `FindOne` 和 `FindOneByXxx` 方法均提供 `*WithTx` 事务变体,确保事务内查询使用 session 连接