|
|
@@ -12,6 +12,10 @@
|
|
|
- **自动权限同步** — 产品启动时通过 API 自动上报权限列表,系统自动新增/更新/禁用
|
|
|
- **JWT 本地验证** — 登录获取 JWT,产品后端可本地验证,无需每次请求回调权限系统
|
|
|
- **登录端隔离** — 产品端(`/auth/login`)和管理后台(`/auth/adminLogin`)独立登录接口,超管仅能通过管理后台登录
|
|
|
+- **令牌轮转** — refreshToken 刷新时自动递增 tokenVersion,旧令牌即时失效,防止令牌被盗后的无限续期
|
|
|
+- **主动注销** — `/auth/logout` 接口递增 tokenVersion 使所有已签发令牌立即失效
|
|
|
+- **产品级熔断** — 禁用产品后所有在线成员的令牌即时失效,HTTP/gRPC 双通道同步拦截
|
|
|
+- **最后管理员保护** — 移除或降级产品最后一个 ADMIN 时自动拒绝,防止产品进入无管理状态
|
|
|
|
|
|
## 系统架构
|
|
|
|
|
|
@@ -156,7 +160,7 @@ erDiagram
|
|
|
└────────────────────────────────────┘
|
|
|
```
|
|
|
|
|
|
-**关键关系**:
|
|
|
+#### 关键关系
|
|
|
|
|
|
1. **用户 → 产品**:通过 `sys_product_member` 建立,一个用户可以是多个产品的成员
|
|
|
2. **用户 → 部门**:通过 `sys_user.deptId` 关联,一个用户只属于一个部门
|
|
|
@@ -178,10 +182,12 @@ erDiagram
|
|
|
|
|
|
```mermaid
|
|
|
flowchart TD
|
|
|
- START[获取用户权限] --> IS_SUPER{是超级管理员?}
|
|
|
+ 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{是产品成员?}
|
|
|
+ FIND_MEMBER --> HAS_MEMBER{是活跃成员?}
|
|
|
HAS_MEMBER -->|否| EMPTY[返回空权限]
|
|
|
HAS_MEMBER -->|是| CHECK_TYPE{成员类型?}
|
|
|
CHECK_TYPE -->|DEVELOPER / ADMIN| ALL_PERMS
|
|
|
@@ -247,7 +253,7 @@ POST /api/product/create {"code": "oa", "name": "OA 系统"}
|
|
|
POST /api/product/create {"code": "mall", "name": "电商后台"}
|
|
|
```
|
|
|
|
|
|
-每个产品创建后会自动生成一个初始管理员账号(如 `admin_crm`),该账号是该产品的 `SUPER_ADMIN` 级别成员。
|
|
|
+每个产品创建后会自动生成一个初始管理员账号(如 `admin_crm`),该账号是该产品的 `ADMIN` 级别成员。
|
|
|
|
|
|
### 第三步:产品上报权限
|
|
|
|
|
|
@@ -372,16 +378,16 @@ flowchart TD
|
|
|
flowchart LR
|
|
|
REQ[HTTP 请求] --> L1["① JWT 中间件<br>解析 token / 验证签名"]
|
|
|
L1 --> L2["② UserDetailsLoader<br>加载用户实时状态"]
|
|
|
- L2 --> L3["③ 冻结检查<br>Status != 1 → 403"]
|
|
|
+ L2 --> L3["③ 多维状态检查<br>用户/产品/成员状态"]
|
|
|
L3 --> L4["④ Logic 层<br>操作权限控制"]
|
|
|
L4 --> BIZ[业务逻辑]
|
|
|
```
|
|
|
|
|
|
| 层级 | 组件 | 职责 |
|
|
|
| ------ | ------ | ------ |
|
|
|
-| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType` |
|
|
|
-| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限) |
|
|
|
-| 第三层 | 用户状态检查 | 冻结账号(`status ≠ 1`)直接返回 403 |
|
|
|
+| 第一层 | JWT 中间件 | 解析 access token、验证签名、校验 `tokenType`、校验 `tokenVersion` |
|
|
|
+| 第二层 | UserDetailsLoader | 从 Redis 缓存或 DB 加载用户完整信息(含部门、角色、权限、产品状态) |
|
|
|
+| 第三层 | 多维状态检查 | 用户冻结(`status ≠ 1`)→ 403;产品禁用(`productStatus ≠ 1`)→ 403;成员无效(`memberType` 为空)→ 403 |
|
|
|
| 第四层 | access.go | 按接口类型检查超管/产品管理员/部门层级/权限级别 |
|
|
|
|
|
|
### 接口操作权限矩阵
|
|
|
@@ -396,27 +402,30 @@ flowchart LR
|
|
|
| 更新部门 | 仅超级管理员 | — |
|
|
|
| 删除部门 | 仅超级管理员 | 有子部门时拒绝 |
|
|
|
| **角色管理** | | |
|
|
|
-| 创建角色 | 超管 或 产品管理员 | — |
|
|
|
-| 更新角色 | 超管 或 产品管理员 | — |
|
|
|
+| 创建角色 | 超管 或 产品管理员 | 产品必须启用 |
|
|
|
+| 更新角色 | 超管 或 产品管理员 | 非超管不可降低 permsLevel |
|
|
|
| 删除角色 | 超管 或 产品管理员 | 级联删除关联数据 |
|
|
|
| 绑定角色权限 | 超管 或 产品管理员 | — |
|
|
|
| **用户管理** | | |
|
|
|
| 创建用户 | 超管 或 产品管理员 | — |
|
|
|
| 更新用户信息 | 仅本人 或 超管 | — |
|
|
|
| 冻结/解冻用户 | 通过 `CheckManageAccess` | 不可冻结自己和超管 |
|
|
|
-| 绑定角色 | 通过 `CheckManageAccess` | — |
|
|
|
-| 设置权限覆盖 | 通过 `CheckManageAccess` | — |
|
|
|
+| 绑定角色 | 通过 `CheckManageAccess` | 目标用户的成员状态必须启用 |
|
|
|
+| 设置权限覆盖 | 通过 `CheckManageAccess` | 目标用户的成员状态必须启用;产品必须启用 |
|
|
|
| **成员管理** | | |
|
|
|
-| 添加成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可分配同级或更高类型 |
|
|
|
-| 更新成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | — |
|
|
|
-| 移除成员 | 通过 `CheckManageAccess` | 级联清理角色/权限绑定 |
|
|
|
+| 添加成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可分配同级或更高类型;产品必须启用 |
|
|
|
+| 更新成员 | 通过 `CheckManageAccess` + `CheckMemberTypeAssignment` | 不可降级最后一个 ADMIN |
|
|
|
+| 移除成员 | 通过 `CheckManageAccess` | 不可移除最后一个 ADMIN;级联清理角色/权限绑定 |
|
|
|
| **查询类接口** | | |
|
|
|
| 产品/部门/角色/用户/成员列表与详情 | 已登录即可 | — |
|
|
|
| 用户信息 (userInfo) | 已登录即可 | 返回当前登录用户自己的信息 |
|
|
|
+| **认证接口** | | |
|
|
|
+| 用户注销 (logout) | 已登录即可 | 递增 tokenVersion,所有已签发令牌即时失效 |
|
|
|
| **公开接口** | | |
|
|
|
| 产品端登录 (login) | 无需鉴权 | 超级管理员被拒绝,productCode 必传 |
|
|
|
-| 管理后台登录 (adminLogin) | 无需鉴权 | 需验证 managementKey |
|
|
|
-| 刷新令牌 / 同步权限 | 无需鉴权 | 同步权限通过 appKey/appSecret 认证 |
|
|
|
+| 管理后台登录 (adminLogin) | 无需鉴权 | 需验证 managementKey,限流在 managementKey 校验之后 |
|
|
|
+| 刷新令牌 | 无需鉴权 | 令牌轮转:旧令牌即时失效,返回全新令牌对;产品禁用时拒绝 |
|
|
|
+| 同步权限 | 无需鉴权 | 通过 appKey/appSecret 认证 |
|
|
|
|
|
|
### CheckManageAccess — 用户管理权限检查
|
|
|
|
|
|
@@ -444,7 +453,7 @@ flowchart TD
|
|
|
CMP_PERM -->|相等或更大| DENY4[❌ 同级或更低]
|
|
|
```
|
|
|
|
|
|
-**检查维度说明**:
|
|
|
+#### 检查维度说明
|
|
|
|
|
|
1. **超管豁免**:`SUPER_ADMIN` 不受任何限制
|
|
|
2. **自我豁免**:操作自己的记录总是允许
|
|
|
@@ -457,18 +466,18 @@ flowchart TD
|
|
|
|
|
|
`UserDetailsLoader` 是一个集中式的用户信息加载与缓存组件,为中间件、登录、用户信息查询等多个场景提供统一的数据来源。
|
|
|
|
|
|
-**加载的完整数据**:
|
|
|
+#### 加载的完整数据
|
|
|
|
|
|
| 数据来源 | 加载的字段 |
|
|
|
| ---------- | ----------- |
|
|
|
| `sys_user` | userId, username, nickname, avatar, email, phone, remark, isSuperAdmin, mustChangePassword, status |
|
|
|
| `sys_dept` | deptId, deptName, deptPath, deptType |
|
|
|
-| `sys_product` | productCode, productName |
|
|
|
+| `sys_product` | productCode, productName, productStatus |
|
|
|
| `sys_product_member` | memberType |
|
|
|
| `sys_role` (当前产品) | roles[], minPermsLevel |
|
|
|
| 计算后的权限 | perms[] (权限 code 集合) |
|
|
|
|
|
|
-**缓存策略**:
|
|
|
+#### 缓存策略
|
|
|
|
|
|
- **存储**:Redis JSON,key 格式 `{prefix}:ud:{userId}:{productCode}`
|
|
|
- **TTL**:300 秒(5 分钟)自然过期
|
|
|
@@ -476,11 +485,12 @@ flowchart TD
|
|
|
|
|
|
| 操作 | 失效方法 | 失效范围 |
|
|
|
| ------ | ---------- | ---------- |
|
|
|
-| 更新用户信息 / 冻结解冻 / 修改密码 | `Clean(userId)` | 该用户所有产品缓存 |
|
|
|
+| 更新用户信息 / 冻结解冻 / 修改密码 / 注销 | `Clean(userId)` | 该用户所有产品缓存 |
|
|
|
| 设置用户权限覆盖 / 添加成员 / 更新成员 | `Del(userId, productCode)` | 该用户在指定产品的缓存 |
|
|
|
| 更新角色 / 删除角色 / 绑定角色权限 | `BatchDel(userIds, productCode)` | 受影响用户在指定产品的缓存 |
|
|
|
| 更新产品 / 同步权限 | `CleanByProduct(productCode)` | 该产品下所有用户的缓存 |
|
|
|
| 更新部门 | `Clean(uid)` × N | 该部门下所有用户的缓存 |
|
|
|
+| 刷新令牌 | `Clean(userId)` | 轮转时递增 tokenVersion 后清除缓存 |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -604,7 +614,7 @@ POST /api/perm/sync
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-**Go 代码示例(放在 main 启动流程中):**
|
|
|
+#### Go 代码示例(放在 main 启动流程中)
|
|
|
|
|
|
```go
|
|
|
var permsList = []map[string]string{
|
|
|
@@ -657,17 +667,19 @@ resp, err := client.Login(ctx, "crm", "zhangsan", "123456")
|
|
|
|
|
|
```go
|
|
|
type Claims struct {
|
|
|
- TokenType string `json:"tokenType"`
|
|
|
- UserId int64 `json:"userId"`
|
|
|
- Username string `json:"username"`
|
|
|
- ProductCode string `json:"productCode"`
|
|
|
- MemberType string `json:"memberType"`
|
|
|
- Perms []string `json:"perms"`
|
|
|
+ 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"`。
|
|
|
+> - `tokenType` 字段区分 `"access"` 和 `"refresh"`,验证时应检查 `tokenType == "access"`
|
|
|
+> - `tokenVersion` 用于令牌吊销:用户注销或刷新令牌时版本递增,旧版本令牌即时失效
|
|
|
|
|
|
#### 3. 业务接口中检查权限
|
|
|
|
|
|
@@ -692,7 +704,7 @@ Content-Type: application/json
|
|
|
{"productCode": "crm"}
|
|
|
```
|
|
|
|
|
|
-> `refreshToken` 有效期默认 7 天,刷新时返回原始 refreshToken(不重新签发),过期后必须重新登录。这确保了 token 有固定的生命周期。
|
|
|
+> `refreshToken` 有效期默认 7 天。系统采用令牌轮转策略:每次刷新都返回全新的 accessToken 和 refreshToken,旧令牌即时失效(通过递增 `tokenVersion` 实现)。过期后必须重新登录。
|
|
|
|
|
|
### 接入检查清单
|
|
|
|
|
|
@@ -702,7 +714,8 @@ Content-Type: application/json
|
|
|
- [ ] 登录接口正确调用权限系统并返回 token
|
|
|
- [ ] JWT 鉴权中间件已加入受保护路由,并验证 `tokenType == "access"`
|
|
|
- [ ] 业务接口中根据 `claims.Perms` 做了权限校验
|
|
|
-- [ ] 前端在 `accessToken` 过期时能自动调用 `refreshToken` 续期
|
|
|
+- [ ] 前端在 `accessToken` 过期时能自动调用 `refreshToken` 续期(注意:刷新后旧令牌失效,需立即替换)
|
|
|
+- [ ] 前端实现注销功能(调用 `/api/auth/logout`),确保令牌在服务端即时吊销
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -752,7 +765,7 @@ Content-Type: application/json
|
|
|
| password | string | 是 | 密码 |
|
|
|
| productCode | string | 是 | 产品编码 |
|
|
|
|
|
|
-**响应 data:**
|
|
|
+#### 响应 data
|
|
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|
| ------ | ------ | ------ |
|
|
|
@@ -782,7 +795,7 @@ Content-Type: application/json
|
|
|
| Authorization | header | 是 | `Bearer {refreshToken}` |
|
|
|
| productCode | string | 否 | 切换产品上下文时传入(Body) |
|
|
|
|
|
|
-**响应 data:** 与登录接口相同。注意返回的 `refreshToken` 是原始值(不重新签发),refresh token 有固定有效期,过期后需重新登录。
|
|
|
+**响应 data:** 与登录接口相同。采用令牌轮转策略:每次刷新都会递增 `tokenVersion`,返回全新的 accessToken 和 refreshToken,旧令牌即时失效。refresh token 有固定有效期,过期后需重新登录。
|
|
|
|
|
|
#### POST /api/perm/sync — 同步产品权限
|
|
|
|
|
|
@@ -801,6 +814,12 @@ Content-Type: application/json
|
|
|
|
|
|
### 认证接口(需鉴权)
|
|
|
|
|
|
+#### POST /api/auth/logout — 用户注销
|
|
|
+
|
|
|
+无请求参数。递增当前用户的 `tokenVersion`,使所有已签发的 access/refresh 令牌立即失效,并清除用户缓存。
|
|
|
+
|
|
|
+**响应 data:** `null`
|
|
|
+
|
|
|
#### POST /api/auth/userInfo — 获取当前用户信息
|
|
|
|
|
|
无请求参数。**响应 data:** `UserInfo` 对象。
|
|
|
@@ -912,6 +931,8 @@ Content-Type: application/json
|
|
|
|
|
|
#### POST /api/role/update — 更新角色
|
|
|
|
|
|
+非超级管理员不可降低角色的 `permsLevel`(即不可将数值改大)。
|
|
|
+
|
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|
|
| ------ | ------ | ------ | ------ |
|
|
|
| id | int64 | 是 | 角色 ID |
|
|
|
@@ -1044,7 +1065,7 @@ Content-Type: application/json
|
|
|
|
|
|
#### POST /api/member/update — 更新成员
|
|
|
|
|
|
-需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。
|
|
|
+需通过 `CheckManageAccess` + `CheckMemberTypeAssignment` 权限检查。降级产品最后一个 ADMIN 时会被拒绝。
|
|
|
|
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|
|
| ------ | ------ | ------ | ------ |
|
|
|
@@ -1054,7 +1075,7 @@ Content-Type: application/json
|
|
|
|
|
|
#### POST /api/member/remove — 移除成员
|
|
|
|
|
|
-需通过 `CheckManageAccess` 权限检查。级联清理该成员在该产品下的角色绑定和权限覆盖。
|
|
|
+需通过 `CheckManageAccess` 权限检查。不可移除产品最后一个 ADMIN。级联清理该成员在该产品下的角色绑定和权限覆盖。
|
|
|
|
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|
|
| ------ | ------ | ------ | ------ |
|
|
|
@@ -1078,10 +1099,10 @@ gRPC 服务定义见 `pb/perm.proto`,默认监听 `:10002`。
|
|
|
|
|
|
| 方法 | 说明 | 使用场景 |
|
|
|
| ------ | ------ | ---------- |
|
|
|
-| `SyncPermissions` | 同步产品权限列表 | 产品启动时调用 |
|
|
|
+| `SyncPermissions` | 同步产品权限列表 | 产品启动时调用,通过 appKey/appSecret 认证 |
|
|
|
| `Login` | 产品端登录 | 产品后端代理用户登录(productCode 必传,超管被拒绝) |
|
|
|
-| `RefreshToken` | 刷新令牌 | accessToken 过期续期 |
|
|
|
-| `VerifyToken` | 验证令牌 | 产品后端验证用户 token(可选,推荐本地 JWT 验证) |
|
|
|
+| `RefreshToken` | 刷新令牌(轮转) | accessToken 过期续期,旧令牌即时失效;产品禁用时拒绝 |
|
|
|
+| `VerifyToken` | 验证令牌 | 产品后端验证用户 token(可选,推荐本地 JWT 验证);产品禁用时拒绝 |
|
|
|
| `GetUserPerms` | 获取用户权限 | 实时查询用户最新权限 |
|
|
|
|
|
|
所有 gRPC 错误使用标准 `status.Error(codes.Xxx, msg)` 格式。
|
|
|
@@ -1131,14 +1152,14 @@ server/
|
|
|
├── loaders/
|
|
|
│ └── userDetailsLoader.go # 用户详情加载器(Redis 缓存 + DB 回源)
|
|
|
├── middleware/
|
|
|
- │ └── jwtauthMiddleware.go # JWT 鉴权中间件(token 验证 + 状态检查 + UserDetails 注入)
|
|
|
+ │ └── 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/ # 认证接口
|
|
|
+ │ ├── pub/ # 公开接口(登录/刷新令牌/同步权限)
|
|
|
+ │ ├── auth/ # 认证接口(用户信息/修改密码/注销)
|
|
|
│ ├── product/ # 产品管理
|
|
|
│ ├── dept/ # 部门管理
|
|
|
│ ├── perm/ # 权限管理
|
|
|
@@ -1195,13 +1216,13 @@ server/
|
|
|
|
|
|
当服务部署在 Nginx 等反向代理后面时,需要开启 `BehindProxy` 配置以正确获取客户端真实 IP(用于登录限流等安全策略):
|
|
|
|
|
|
-**1. 配置文件设置**
|
|
|
+#### 1. 配置文件设置
|
|
|
|
|
|
```yaml
|
|
|
BehindProxy: true
|
|
|
```
|
|
|
|
|
|
-**2. Nginx 配置要求**
|
|
|
+#### 2. Nginx 配置要求
|
|
|
|
|
|
必须在 Nginx 中正确设置 `X-Real-IP` 并清除客户端可伪造的 `X-Forwarded-For`:
|
|
|
|
|
|
@@ -1216,7 +1237,7 @@ server {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-**安全说明**:
|
|
|
+#### 安全说明
|
|
|
|
|
|
- `BehindProxy: false`(默认)— 仅使用 TCP 连接的 `RemoteAddr` 作为客户端 IP,适合服务直接暴露或在无法信任代理头的场景
|
|
|
- `BehindProxy: true` — 信任 Nginx 设置的 `X-Real-IP` 头获取真实客户端 IP,**必须**确保 Nginx 正确配置且外部无法绕过 Nginx 直连后端
|
|
|
@@ -1276,7 +1297,7 @@ server {
|
|
|
./run-test.sh -h
|
|
|
```
|
|
|
|
|
|
-**环境变量**:
|
|
|
+#### 环境变量
|
|
|
|
|
|
| 变量 | 默认值 | 说明 |
|
|
|
| ------ | -------- | ------ |
|