|
|
@@ -0,0 +1,681 @@
|
|
|
+# 统一认证平台(LQAI-middle-platform)接入流程及 API 接口文档
|
|
|
+
|
|
|
+> **参考样本**: 样本中心(LQAdminPlatform)已实现的统一认证接入方案
|
|
|
+> **统一认证平台地址**: `http://192.168.92.61:8200`
|
|
|
+> **统一认证平台前端**: `http://192.168.92.61:9200`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、系统架构概览
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────────┐
|
|
|
+│ 统一认证平台 (LQAI-middle-platform) │
|
|
|
+│ 端口: 8200 (后端) / 9200 (前端) │
|
|
|
+│ │
|
|
|
+│ 功能: │
|
|
|
+│ • 用户注册 / 登录 / 密码管理 │
|
|
|
+│ • 应用管理 (子应用注册、client_id/secret 生成) │
|
|
|
+│ • OAuth2 授权码流程 │
|
|
|
+│ • RBAC 角色权限管理 │
|
|
|
+│ • JWT Token 签发与验证 │
|
|
|
+│ • 子应用工作台(点击子应用图标触发 SSO 跳转) │
|
|
|
+│ │
|
|
|
+│ 数据存储: MySQL (lq_ai_middle_platform) + Redis │
|
|
|
+└──────────────────────┬──────────────────────────────────────┘
|
|
|
+ │ OAuth2 / HTTP
|
|
|
+ │
|
|
|
+ ┌──────────────┼──────────────┐
|
|
|
+ │ │ │
|
|
|
+ ┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐
|
|
|
+ │样本中心 │ │Agent平台 │ │ 标注平台 │ ... 其他子系统
|
|
|
+ │(8000) │ │ (8002) │ │ (9003) │
|
|
|
+ └─────────┘ └───────────┘ └─────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 角色定位
|
|
|
+
|
|
|
+| 角色 | 说明 |
|
|
|
+|------|------|
|
|
|
+| **统一认证平台** (LQAI-middle-platform) | OAuth2 授权服务器(Authorization Server),同时也是一个 SSO 中心 |
|
|
|
+| **子系统** (如样本中心 LQAdminPlatform) | OAuth2 客户端(Client),同时自身也是一个小型 OAuth2 服务器,可以为下游系统提供授权 |
|
|
|
+| **子系统前端** | 负责 SSO 回调处理和 Token 交换 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、SSO 单点登录接入流程
|
|
|
+
|
|
|
+### 2.1 整体流程图
|
|
|
+
|
|
|
+```
|
|
|
+用户在统一认证平台工作台点击子系统图标
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+① 统一认证平台后端生成 OAuth2 授权码 code
|
|
|
+ POST /auth/sso-redirect → 返回重定向URL: {frontend_url}/auth/callback?code=xxx
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+② 302 重定向到子系统前端 /auth/callback?code=xxx
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+③ 子系统前端 POST /api/oauth/exchange-code { "code": "xxx" }
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+④ 子系统后端与统一认证平台进行后端到后端通信:
|
|
|
+ 4a. POST {SSO_BASE_URL}/oauth/token → 换取 SSO access_token
|
|
|
+ 4b. GET {SSO_BASE_URL}/oauth/userinfo → 获取用户信息+角色
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+⑤ 子系统后端同步用户到本地数据库(查找或创建用户、同步角色)
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+⑥ 子系统后端签发本地 JWT,返回 { token, refresh_token, user }
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+⑦ 子系统前端保存 Token,跳转首页
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 详细步骤说明
|
|
|
+
|
|
|
+#### Step 1: 统一认证平台生成授权码
|
|
|
+
|
|
|
+当用户在统一认证平台前端工作台的"应用列表"中点击某个子系统图标时:
|
|
|
+
|
|
|
+- 统一认证平台后端调用 `POST /auth/sso-redirect` 接口
|
|
|
+- 后端为当前登录用户生成一个 OAuth2 授权码(authorization code),存入 Redis,有效期 10 分钟
|
|
|
+- 返回子系统的回调 URL,格式为 `{REDIRECT_URI}?code={auth_code}`
|
|
|
+
|
|
|
+#### Step 2: 重定向到子系统前端
|
|
|
+
|
|
|
+- 统一认证平台前端通过 302 重定向将用户引导至子系统前端的回调页面
|
|
|
+- URL 格式: `http://localhost:3000/auth/callback?code=xxxxxx`
|
|
|
+
|
|
|
+#### Step 3: 子系统前端调用换码接口
|
|
|
+
|
|
|
+子系统前端从 URL 参数中提取 `code`,调用子系统后端的换码接口:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 示例代码
|
|
|
+const code = new URLSearchParams(window.location.search).get('code');
|
|
|
+const response = await fetch('/api/oauth/exchange-code', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ code })
|
|
|
+});
|
|
|
+const result = await response.json();
|
|
|
+// result.data.token → 本地 JWT access_token
|
|
|
+// result.data.refresh_token → 刷新令牌
|
|
|
+// result.data.user → 用户信息(含角色)
|
|
|
+```
|
|
|
+
|
|
|
+#### Step 4-5: 子系统后端与统一认证平台交互 + 同步用户
|
|
|
+
|
|
|
+子系统后端收到 `code` 后执行 `_sso_exchange_code` 核心流程:
|
|
|
+
|
|
|
+1. **用 code 换 SSO access_token** — 调用统一认证平台的 `/oauth/token` 端点
|
|
|
+2. **获取用户信息** — 调用统一认证平台的 `/oauth/userinfo` 端点
|
|
|
+3. **同步用户** — 在本地数据库查找或创建用户,同步角色信息
|
|
|
+
|
|
|
+#### Step 6-7: 签发本地 JWT 并返回
|
|
|
+
|
|
|
+- 子系统后端用自己的 `SECRET_KEY` 签发本地 JWT
|
|
|
+- 返回给前端 `{ token, refresh_token, user }`
|
|
|
+- 前端保存 Token 到 localStorage/sessionStorage,跳转首页
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、子系统接入前准备
|
|
|
+
|
|
|
+### 3.1 在统一认证平台注册子应用
|
|
|
+
|
|
|
+通过统一认证平台的系统管理模块(或数据库直接插入)在 `t_sys_app` 表中注册子系统:
|
|
|
+
|
|
|
+| 字段 | 说明 | 示例值 |
|
|
|
+|------|------|--------|
|
|
|
+| `name` | 子系统名称 | "样本中心" |
|
|
|
+| `app_key` | 客户端 ID(client_id),32 位随机字符串 | `WviiGL8KQE20tQhmhQPQhhJ5QpFK51F6` |
|
|
|
+| `app_secret` | 客户端密钥(client_secret),64 位随机字符串 | `9WXP88hEHJiHRSiUdmx7ip5oQPzY0bnJNsEswQoO4sk6juCplyJTcnAiZsv7e3lJ` |
|
|
|
+| `redirect_uris` | 允许的重定向 URI(JSON 数组) | `["http://localhost:3000/auth/callback"]` |
|
|
|
+| `scope` | 授权范围(JSON 数组) | `["profile", "email"]` |
|
|
|
+| `is_active` | 是否启用 | `1` |
|
|
|
+| `is_trusted` | 是否信任(跳过授权确认页) | `1` |
|
|
|
+| `home_url` | 子系统首页 URL | `http://localhost:3000` |
|
|
|
+| `icon_url` | 子系统图标 URL | 可选 |
|
|
|
+
|
|
|
+### 3.2 子系统侧配置
|
|
|
+
|
|
|
+在子系统的配置文件中添加 SSO 配置段:
|
|
|
+
|
|
|
+```ini
|
|
|
+[sso]
|
|
|
+SSO_BASE_URL=http://192.168.92.61:8200
|
|
|
+CLIENT_ID=WviiGL8KQE20tQhmhQPQhhJ5QpFK51F6
|
|
|
+CLIENT_SECRET=9WXP88hEHJiHRSiUdmx7ip5oQPzY0bnJNsEswQoO4sk6juCplyJTcnAiZsv7e3lJ
|
|
|
+REDIRECT_URI=http://localhost:3000/auth/callback
|
|
|
+FRONTEND_URL=http://localhost:3000
|
|
|
+SCOPE=email
|
|
|
+SSO_LOGOUT_REDIRECT_URL=http://192.168.92.61:9200/login
|
|
|
+```
|
|
|
+
|
|
|
+> **注意**: `CLIENT_ID` 和 `CLIENT_SECRET` 必须与统一认证平台 `t_sys_app` 表中注册的 `app_key` 和 `app_secret` 一致。
|
|
|
+
|
|
|
+### 3.3 角色映射
|
|
|
+
|
|
|
+子系统需要确保本地 `t_sys_role` 表中存在与统一认证平台对应的角色。样本中心的角色包括:
|
|
|
+
|
|
|
+| 统一认证平台角色 code | 说明 |
|
|
|
+|----------------------|------|
|
|
|
+| `super_admin` | 超级管理员 |
|
|
|
+| `sam_sys_admin` | 样本中心系统管理员 |
|
|
|
+| `sam_data_operator` | 样本中心数据操作员 |
|
|
|
+
|
|
|
+统一认证平台 `/oauth/userinfo` 返回的 `roles` 字段格式为对象数组:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "roles": [
|
|
|
+ { "name": "超级管理员", "code": "super_admin" },
|
|
|
+ { "name": "样本中心系统管理员", "code": "sam_sys_admin" }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+子系统从每个对象中提取 `code` 字段进行角色匹配和本地同步。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、统一认证平台核心 API 接口
|
|
|
+
|
|
|
+以下接口由 **统一认证平台(LQAI-middle-platform,端口 8200)** 提供,子系统需要调用。
|
|
|
+
|
|
|
+### 4.1 获取 SSO 授权 URL
|
|
|
+
|
|
|
+> 供子系统前端构建"跳转到统一认证平台登录"的链接
|
|
|
+
|
|
|
+- **接口**: `GET /oauth/authorize`
|
|
|
+- **认证**: 不需要
|
|
|
+
|
|
|
+| 参数 | 类型 | 必填 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `response_type` | string | 是 | 固定值 `code` |
|
|
|
+| `client_id` | string | 是 | 子应用的 app_key |
|
|
|
+| `redirect_uri` | string | 是 | 回调地址(必须在 t_sys_app 中注册过) |
|
|
|
+| `scope` | string | 否 | 授权范围,空格分隔,默认 `profile` |
|
|
|
+| `state` | string | 否 | 防 CSRF 状态参数 |
|
|
|
+
|
|
|
+**响应**: 302 重定向(如果用户已登录则重定向到 `redirect_uri?code=xxx`,未登录则重定向到登录页面)
|
|
|
+
|
|
|
+### 4.2 获取授权码 URL(SSO 免登)
|
|
|
+
|
|
|
+> 统一认证平台内部使用,当用户点击子系统图标时调用
|
|
|
+
|
|
|
+- **接口**: `POST /auth/sso-redirect`
|
|
|
+- **认证**: 需要用户已登录统一认证平台
|
|
|
+- **请求体**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "client_id": "WviiGL8KQE20tQhmhQPQhhJ5QpFK51F6",
|
|
|
+ "redirect_uri": "http://localhost:3000/auth/callback"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- **响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "redirect_url": "http://localhost:3000/auth/callback?code=xxxxxx"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 令牌交换端点
|
|
|
+
|
|
|
+> **子系统后端调用此接口,用授权码换取 SSO access_token**
|
|
|
+
|
|
|
+- **接口**: `POST /oauth/token`
|
|
|
+- **Content-Type**: `application/x-www-form-urlencoded`
|
|
|
+- **认证**: 不需要
|
|
|
+
|
|
|
+| 参数 | 类型 | 必填 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `grant_type` | string | 是 | 固定值 `authorization_code` |
|
|
|
+| `code` | string | 是 | 授权码 |
|
|
|
+| `redirect_uri` | string | 是 | 与 Step 1 中使用的 redirect_uri 一致 |
|
|
|
+| `client_id` | string | 是 | 子应用的 app_key |
|
|
|
+| `client_secret` | string | 是 | 子应用的 app_secret |
|
|
|
+
|
|
|
+**成功响应** (200):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
|
+ "token_type": "Bearer",
|
|
|
+ "expires_in": 7200,
|
|
|
+ "refresh_token": "xxxxxxxxxxxxxxxxxxxx",
|
|
|
+ "scope": "profile email"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**失败响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "error": "invalid_grant",
|
|
|
+ "error_description": "授权码无效"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| error 值 | 说明 |
|
|
|
+|----------|------|
|
|
|
+| `invalid_request` | 缺少必填参数 |
|
|
|
+| `unsupported_grant_type` | grant_type 不支持 |
|
|
|
+| `invalid_client` | client_id 或 client_secret 错误 |
|
|
|
+| `invalid_grant` | 授权码无效/已使用/过期,或 redirect_uri 不匹配 |
|
|
|
+
|
|
|
+### 4.4 获取用户信息
|
|
|
+
|
|
|
+> **子系统后端调用此接口,获取当前登录用户的详细信息**
|
|
|
+
|
|
|
+- **接口**: `GET /oauth/userinfo`
|
|
|
+- **认证**: Bearer Token(使用 4.3 返回的 access_token)
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+```
|
|
|
+Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
|
|
+```
|
|
|
+
|
|
|
+**成功响应** (200):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "sub": "user_xxx",
|
|
|
+ "username": "admin",
|
|
|
+ "email": "admin@example.com",
|
|
|
+ "avatar_url": "https://...",
|
|
|
+ "real_name": "管理员",
|
|
|
+ "company": "四川路桥",
|
|
|
+ "department": "技术部",
|
|
|
+ "position": "工程师",
|
|
|
+ "roles": [
|
|
|
+ { "name": "超级管理员", "code": "super_admin" },
|
|
|
+ { "name": "样本中心系统管理员", "code": "sam_sys_admin" }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 返回的字段根据 token 的 scope 过滤:`profile` scope 返回基本信息,`email` scope 返回邮箱,`phone` scope 返回手机号。
|
|
|
+
|
|
|
+**失败响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "error": "invalid_token",
|
|
|
+ "error_description": "User not found or inactive"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、子系统侧需要实现的 API 接口
|
|
|
+
|
|
|
+以下是子系统(如样本中心)自身需要实现的接口,用于完成 SSO 接入。
|
|
|
+
|
|
|
+### 5.1 授权码交换接口(核心免登接口)
|
|
|
+
|
|
|
+- **接口**: `POST /api/oauth/exchange-code`
|
|
|
+- **Content-Type**: `application/json`
|
|
|
+- **认证**: 不需要
|
|
|
+- **用途**: 前端收到 SSO 回调的 code 后,调用此接口换取本地 JWT
|
|
|
+
|
|
|
+**请求体**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "xxxxxx"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**成功响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "message": "登录成功",
|
|
|
+ "data": {
|
|
|
+ "token": "eyJhbGciOiJIUzI1NiIs...",
|
|
|
+ "refresh_token": "xxxxxxxxxxxxxxxxxxxx",
|
|
|
+ "user": {
|
|
|
+ "id": "user_xxx",
|
|
|
+ "username": "admin",
|
|
|
+ "email": "admin@example.com",
|
|
|
+ "phone": "138xxxx",
|
|
|
+ "is_superuser": true,
|
|
|
+ "is_active": true,
|
|
|
+ "roles": ["super_admin", "sam_sys_admin"]
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**失败响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "100001",
|
|
|
+ "message": "缺少授权码",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "500001",
|
|
|
+ "message": "登录失败: 获取令牌失败",
|
|
|
+ "data": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 获取 SSO 授权 URL
|
|
|
+
|
|
|
+- **接口**: `GET /auth/sso/authorize`
|
|
|
+- **认证**: 不需要
|
|
|
+
|
|
|
+| 参数 | 类型 | 必填 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `redirect` | bool | 否 | 为 true 时直接 302 重定向到 SSO 授权页 |
|
|
|
+
|
|
|
+**响应** (redirect=false):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "message": "获取授权URL成功",
|
|
|
+ "data": {
|
|
|
+ "authorize_url": "http://192.168.92.61:8200/oauth/authorize?client_id=xxx&redirect_uri=xxx&response_type=code&scope=email"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.3 SSO 回调端点(旧流程,后端 302 重定向方式)
|
|
|
+
|
|
|
+- **接口**: `GET /auth/callback`
|
|
|
+- **认证**: 不需要
|
|
|
+
|
|
|
+| 参数 | 类型 | 必填 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `code` | string | 是 | SSO 授权码 |
|
|
|
+| `error` | string | 否 | SSO 返回的错误码 |
|
|
|
+| `error_description` | string | 否 | 错误描述 |
|
|
|
+| `state` | string | 否 | 状态参数 |
|
|
|
+
|
|
|
+**响应**: 302 重定向到 `前端URL/oauth/callback?token=xxx&refresh_token=xxx`
|
|
|
+
|
|
|
+> **注意**: 这是旧流程,建议使用 `POST /api/oauth/exchange-code` 的新流程(前端直接调用 API 换码,不依赖后端 302 重定向)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、子系统侧的其他认证相关 API
|
|
|
+
|
|
|
+除了 SSO 接入外,子系统还需要提供标准认证接口供前端使用。
|
|
|
+
|
|
|
+### 6.1 本地密码登录
|
|
|
+
|
|
|
+- **接口**: `POST /api/v1/auth/login`
|
|
|
+- **Content-Type**: `application/json`
|
|
|
+
|
|
|
+**请求体**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "username": "admin",
|
|
|
+ "password": "Admin123456",
|
|
|
+ "remember_me": false
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "message": "登录成功",
|
|
|
+ "data": {
|
|
|
+ "access_token": "eyJ...",
|
|
|
+ "refresh_token": "xxx...",
|
|
|
+ "token_type": "Bearer",
|
|
|
+ "expires_in": 1200
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 刷新 Token
|
|
|
+
|
|
|
+- **接口**: `POST /api/v1/auth/refresh`
|
|
|
+- **请求体**: `{ "refresh_token": "xxx" }`
|
|
|
+
|
|
|
+### 6.3 登出
|
|
|
+
|
|
|
+- **接口**: `POST /api/v1/auth/logout`
|
|
|
+- **请求体**: `{ "token": "xxx", "refresh_token": "xxx" }`
|
|
|
+- **响应** (含 SSO 登出重定向 URL):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "message": "登出成功",
|
|
|
+ "data": {
|
|
|
+ "sso_logout_url": "http://192.168.92.61:9200/login"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.4 获取当前用户信息
|
|
|
+
|
|
|
+- **接口**: `GET /api/v1/auth/userinfo` 或 `GET /api/v1/auth/me`
|
|
|
+- **认证**: Bearer Token
|
|
|
+- **响应**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "data": {
|
|
|
+ "id": "user_xxx",
|
|
|
+ "username": "admin",
|
|
|
+ "email": "admin@example.com",
|
|
|
+ "phone": "138xxxx",
|
|
|
+ "roles": ["super_admin"],
|
|
|
+ "permissions": []
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.5 获取验证码
|
|
|
+
|
|
|
+- **接口**: `GET /api/v1/auth/captcha`
|
|
|
+- **认证**: 不需要
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、统一认证平台的 OAuth2 服务端 API
|
|
|
+
|
|
|
+统一认证平台自身也是一个 OAuth2 授权服务器,可以为下游子系统提供 OAuth2 服务。
|
|
|
+
|
|
|
+### 7.1 应用管理(系统管理员操作)
|
|
|
+
|
|
|
+通过统一认证平台的系统管理模块管理子应用,相关接口前缀 `/api/v1`:
|
|
|
+
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `GET` | `/api/v1/system/app/list` | 获取应用列表 |
|
|
|
+| `POST` | `/api/v1/system/app/create` | 创建应用(自动生成 app_key/app_secret) |
|
|
|
+| `PUT` | `/api/v1/system/app/update/{id}` | 更新应用信息 |
|
|
|
+| `DELETE` | `/api/v1/system/app/delete/{id}` | 删除应用 |
|
|
|
+| `POST` | `/api/v1/system/app/reset_secret/{id}` | 重置应用密钥 |
|
|
|
+| `GET` | `/api/v1/system/app/accessible` | 获取当前用户可访问的应用列表 |
|
|
|
+
|
|
|
+### 7.2 角色管理
|
|
|
+
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `GET` | `/api/v1/system/role/list` | 获取角色列表 |
|
|
|
+| `POST` | `/api/v1/system/role/create` | 创建角色 |
|
|
|
+| `PUT` | `/api/v1/system/role/update/{id}` | 更新角色 |
|
|
|
+| `DELETE` | `/api/v1/system/role/delete/{id}` | 删除角色 |
|
|
|
+| `GET` | `/api/external/v1/system/role/user_list/{role_code}` | 按角色 code 查询用户列表 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、Token 机制说明
|
|
|
+
|
|
|
+### 8.1 统一认证平台侧 Token
|
|
|
+
|
|
|
+| Token 类型 | 有效期 | 存储方式 | 说明 |
|
|
|
+|-----------|--------|---------|------|
|
|
|
+| Access Token | 30 分钟(默认)| Redis (`auth:access:{user_id}:{token}`) | 访问令牌,含用户身份和角色 |
|
|
|
+| Refresh Token | 30 天 | Redis (`auth:refresh:{user_id}:{token}`) | 刷新令牌,用于换取新 Access Token |
|
|
|
+| OAuth Access Token | 120 分钟 | Redis (`auth:oauth_access:{client_id}:{token}`) | OAuth2 流程中颁发给子应用的令牌 |
|
|
|
+| OAuth Authorization Code | 10 分钟 | Redis (`auth:oauth_code:{code}`) | 一次性授权码,使用后即失效 |
|
|
|
+
|
|
|
+### 8.2 子系统侧 Token
|
|
|
+
|
|
|
+| Token 类型 | 有效期 | 存储方式 | 说明 |
|
|
|
+|-----------|--------|---------|------|
|
|
|
+| Access Token | 20 分钟 | MySQL + Redis | 子系统本地登录令牌 |
|
|
|
+| Refresh Token | 24 小时 | MySQL + Redis | 刷新令牌 |
|
|
|
+
|
|
|
+### 8.3 滑动过期机制
|
|
|
+
|
|
|
+子系统实现了 JWT Token 的滑动过期机制:
|
|
|
+
|
|
|
+- 当 Token 使用时间超过其总生命周期的 **50%** 时,中间件会自动生成新 Token
|
|
|
+- 新 Token 通过响应头 `X-New-Token` 返回给前端
|
|
|
+- 前端检测到 `X-New-Token` 后替换本地旧 Token
|
|
|
+- 无需前端主动刷新即可保持登录状态
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、前端接入参考代码
|
|
|
+
|
|
|
+### 9.1 SSO 免登流程(推荐方式)
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 1. 统一认证平台重定向到子系统前端,URL 携带 code 参数
|
|
|
+// 例如: http://localhost:3000/auth/callback?code=xxxxxx
|
|
|
+
|
|
|
+// 2. 在回调页面中提取 code 并调用换码接口
|
|
|
+const code = new URLSearchParams(window.location.search).get('code');
|
|
|
+
|
|
|
+if (code) {
|
|
|
+ const response = await fetch('/api/oauth/exchange-code', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ code })
|
|
|
+ });
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === '000000') {
|
|
|
+ // 3. 保存 Token
|
|
|
+ localStorage.setItem('token', result.data.token);
|
|
|
+ localStorage.setItem('refresh_token', result.data.refresh_token);
|
|
|
+ localStorage.setItem('user', JSON.stringify(result.data.user));
|
|
|
+
|
|
|
+ // 4. 跳转到首页
|
|
|
+ router.push('/home');
|
|
|
+ } else {
|
|
|
+ // 登录失败,跳回登录页
|
|
|
+ router.push(`/login?error=${result.message}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 前端请求拦截器(携带 Token)
|
|
|
+
|
|
|
+```typescript
|
|
|
+// Axios 请求拦截器示例
|
|
|
+axios.interceptors.request.use(config => {
|
|
|
+ const token = localStorage.getItem('token');
|
|
|
+ if (token) {
|
|
|
+ config.headers.Authorization = `Bearer ${token}`;
|
|
|
+ }
|
|
|
+ return config;
|
|
|
+});
|
|
|
+
|
|
|
+// 响应拦截器处理 Token 刷新
|
|
|
+axios.interceptors.response.use(
|
|
|
+ response => {
|
|
|
+ const newToken = response.headers['x-new-token'];
|
|
|
+ if (newToken) {
|
|
|
+ localStorage.setItem('token', newToken);
|
|
|
+ }
|
|
|
+ return response;
|
|
|
+ },
|
|
|
+ error => {
|
|
|
+ if (error.response?.status === 401) {
|
|
|
+ // Token 过期,清除并跳转登录
|
|
|
+ localStorage.removeItem('token');
|
|
|
+ localStorage.removeItem('refresh_token');
|
|
|
+ router.push('/login');
|
|
|
+ }
|
|
|
+ return Promise.reject(error);
|
|
|
+ }
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十、接入 Checklist
|
|
|
+
|
|
|
+- [ ] 在统一认证平台注册子应用,获取 `client_id` 和 `client_secret`
|
|
|
+- [ ] 在子应用配置文件中添加 `[sso]` 配置段
|
|
|
+- [ ] 子应用后端实现 `POST /api/oauth/exchange-code` 换码接口
|
|
|
+- [ ] 子应用前端实现回调页面 `/auth/callback`,从 URL 提取 code 并调换码接口
|
|
|
+- [ ] 子应用本地数据库初始化角色表(`t_sys_role`),确保角色 code 与统一认证平台一致
|
|
|
+- [ ] 子应用配置 CORS 允许前端域名访问
|
|
|
+- [ ] 配置 Token 滑动过期中间件
|
|
|
+- [ ] 配置登出时返回 SSO 登出重定向 URL
|
|
|
+- [ ] 测试完整 SSO 流程:统一认证平台登录 → 点击子系统 → 自动免登进入子系统
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十一、错误码说明
|
|
|
+
|
|
|
+### 统一响应格式
|
|
|
+
|
|
|
+所有接口使用统一的响应格式:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": "000000",
|
|
|
+ "message": "success",
|
|
|
+ "data": { ... },
|
|
|
+ "timestamp": "2026-05-10T12:00:00Z"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 业务错误码
|
|
|
+
|
|
|
+| 错误码 | 说明 |
|
|
|
+|--------|------|
|
|
|
+| `000000` | 成功 |
|
|
|
+| `100001` | 缺少授权码 |
|
|
|
+| `400001` | SSO 授权码无效 |
|
|
|
+| `400002` | SSO 用户信息格式异常 |
|
|
|
+| `500001` | 服务器内部错误 |
|
|
|
+
|
|
|
+### OAuth2 标准错误
|
|
|
+
|
|
|
+| error | 说明 |
|
|
|
+|-------|------|
|
|
|
+| `invalid_request` | 请求参数不完整或格式错误 |
|
|
|
+| `invalid_client` | client_id 或 client_secret 错误 |
|
|
|
+| `invalid_grant` | 授权码无效、已使用、过期或 redirect_uri 不匹配 |
|
|
|
+| `invalid_scope` | 请求的 scope 超出应用注册范围 |
|
|
|
+| `unsupported_response_type` | response_type 不支持(仅支持 code) |
|
|
|
+| `unsupported_grant_type` | grant_type 不支持(仅支持 authorization_code) |
|
|
|
+| `access_denied` | 用户拒绝授权 |
|
|
|
+| `server_error` | 服务器内部错误 |
|