c576053ab6
按用户反馈调整: - 删除:app_secret_text / app_secret_file_path / private/public_key 的 text+file_path - 新增:certificates JSON(多 p12 证书,每项含 name/path/password) - 简单凭证(app_id/app_secret/app_key/aes_key)保留单值 VARCHAR 列 - §2 原则 4 改为'简单文本用列 / 多证书用 JSON'
10 KiB
10 KiB
第三方应用管理(SysApp)设计文档
日期: 2026-06-07 状态: 已批准,待实现 作者: AI Assistant
1. 背景与目标
1.1 现状
rui-common-core 中已有 AppProperties 通用第三方应用 POJO(含 appId/appSecret/appKey/privateKey/publicKey/redirectUri)。
rui-common-oauth2 通过 @Bean + @ConfigurationProperties 读取 thirdparty.wechat.* / thirdparty.alipay.* 配置,固定为系统级配置。
问题:
- 普通租户无法管理自己的第三方应用凭证
- 配置硬编码在 Nacos,修改需要运维介入
- 字段不够用(缺支付平台字段、AES key、证书文件等)
- 无法区分"平台默认配置"和"租户自配"
1.2 目标
- 在
rui-service-system增加SysApp实体 + CRUD + 内部接口 - 支持多租户:每条记录区分
owner_type=PLATFORM(平台默认)或TENANT(租户自配) - 凭证字段完整:覆盖社交登录 + 第三方支付 + AES 对称加密 + 证书文件
OAuth2ServerConfig改为运行时从SysApp拉凭证,Redis 缓存 30min- 租户隔离按
client_id → tenant_id映射
2. 核心设计原则
- 多租户隔离:通过
owner_type+tenant_id双重区分 - 凭证集中管理:一个
SysApp记录 = 一个第三方应用在本系统的接入凭证 - 缓存优先:高频读取走 Redis,CRUD 操作失效缓存
- 简单文本用列 / 多证书用 JSON:简单凭证(app_id、app_secret、app_key、aes_key)直接用 VARCHAR 列;多证书场景(如支付宝 p12 包含 private_key+public_key+证书链)用 JSON 数组存,每项含
name/path/password - 兼容现有
AppProperties:AppProperties仍作为通用 POJO 留在rui-common-core,但实际数据从 DB 加载后映射到AppProperties(简单字段直接映射;certificates数组由调用方单独处理) - 加密占位:预留
is_encrypted字段,暂不实现 AES 加密逻辑
3. 数据库设计
3.1 表 sys_app
CREATE TABLE sys_app (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID 0:系统级',
owner_type VARCHAR(20) NOT NULL COMMENT '所有者类型 PLATFORM/TENANT',
platform VARCHAR(50) NOT NULL COMMENT '平台编码 wechat/alipay/stripe',
name VARCHAR(100) NOT NULL COMMENT '管理用名称',
-- 凭证:app_id / app_secret / app_key
app_id VARCHAR(200) DEFAULT NULL COMMENT '应用ID',
app_secret VARCHAR(500) DEFAULT NULL COMMENT '应用密钥',
app_key VARCHAR(200) DEFAULT NULL COMMENT '应用Key(部分平台如支付宝)',
-- 多证书:支付宝 p12 等含 private_key+public_key+证书链的复合证书
-- 每项:{name, path, password},path 存对象存储相对路径
certificates JSON DEFAULT NULL COMMENT '多证书列表(p12等复合证书)',
-- 应用自定义 AES key
aes_key VARCHAR(100) DEFAULT NULL COMMENT '应用AES对称密钥(16/24/32字节)',
-- 通用
redirect_uri VARCHAR(500) DEFAULT NULL COMMENT 'OAuth2授权回调地址',
-- 支付平台专用
merchant_id VARCHAR(100) DEFAULT NULL COMMENT '商户号',
sign_type VARCHAR(20) DEFAULT NULL COMMENT '签名方式 RSA2/MD5/HMAC',
notify_url VARCHAR(500) DEFAULT NULL COMMENT '支付回调URL',
api_base VARCHAR(500) DEFAULT NULL COMMENT 'API根地址',
is_sandbox TINYINT NOT NULL DEFAULT 0 COMMENT '是否沙箱环境 0:否 1:是',
extra JSON DEFAULT NULL COMMENT '扩展字段',
-- 状态与审计
is_encrypted TINYINT NOT NULL DEFAULT 0 COMMENT '预留:是否加密 0:否 1:是(暂不实现)',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0:禁用 1:启用',
description VARCHAR(500) DEFAULT NULL COMMENT '备注',
sort_no INT NOT NULL DEFAULT 0 COMMENT '排序号',
created_by BIGINT DEFAULT NULL COMMENT '创建者ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_by BIGINT DEFAULT NULL COMMENT '更新者ID',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_owner_platform (tenant_id, owner_type, platform, status),
INDEX idx_tenant (tenant_id),
INDEX idx_platform (platform),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方应用集成';
注:
status参与唯一约束,避免"软删除"后还能插同 key。逻辑删除在BaseEntity里有deleted字段。
3.2 枚举 SysAppOwnerType
| 值 | 说明 |
|---|---|
PLATFORM |
平台默认(tenant_id=0),所有租户可继承使用 |
TENANT |
租户自配,覆盖对应 PLATFORM 默认 |
4. Java 包结构
com.rui.service.system/
├── entity/
│ ├── SysApp.java
│ └── SysAppOwnerType.java # 枚举 PLATFORM/TENANT
├── mapper/
│ └── SysAppMapper.java
├── service/
│ ├── ISysAppService.java
│ └── impl/SysAppServiceImpl.java
├── dto/
│ ├── SysAppDTO.java # 详情响应(app_secret 脱敏为 ******)
│ ├── SysAppSaveDTO.java # 创建/更新
│ └── SysAppQueryDTO.java # 分页查询
├── vo/
│ └── AppCredentialsVO.java # 给 oauth2 用的精简视图(仅凭证字段)
└── controller/
├── SysAppController.java # /system/app/** (管理后台 CRUD)
└── inner/
└── SysAppInnerController.java # /system/inner/app/** (oauth2 Feign 调用)
rui-common-oauth2 增加:
com.rui.common.oauth2.feign/
└── SysAppFeign.java # @FeignClient 调用 system
com.rui.common.oauth2.cache/
└── AppCredentialsCache.java # Redis 包装,30min TTL
rui-common-core 保持 AppProperties 不变(POJO 通用载体)。
5. 关键流程
5.1 OAuth2 登录运行时获取凭证
1. POST /oauth2/token?grant_type=wechat&code=xxx
↓
2. authorizationServerFilterChain 被调用
↓
3. 从 OAuth2ClientAuthenticationToken 取 clientId
↓
4. 查 rui_auth_oauth2_client 表 → 取 tenantId
↓
5. AppCredentialsCache.get(platform=wechat, tenantId)
├─ HIT → 直接返回
└─ MISS → Feign: GET /system/inner/app/getCredentials?platform=wechat&tenantId={tenantId}
↓
SysAppInnerController 查 sys_app:
- 优先 owner_type=TENANT AND tenant_id={tid} AND status=1
- 缺省 owner_type=PLATFORM AND tenant_id=0 AND status=1
- 都找不到 → 返回 404(throw new UsernameNotFoundException 或 BizException)
↓
把 SysApp 转 AppCredentialsVO 返回
↓
oauth2 端写 Redis: SET app:creds:{platform}:{tenantId} = json EX 1800
↓
6. 拿到 AppCredentialsVO → 构造 WechatApiClient(appId, appSecret) 等
↓
7. 走完登录流程,返回 token
5.2 租户管理自己的 SysApp
1. 租户管理员登录管理后台
2. POST /system/app (Body: SysAppSaveDTO)
- owner_type=TENANT
- platform=wechat
- tenant_id = 当前用户 tenantId
- 业务校验:uk_tenant_owner_platform 唯一性
3. 写 DB
4. 删缓存:DEL app:creds:wechat:{tenantId}
5. 返回成功
5.3 超管配置 PLATFORM 默认
1. 超管登录管理后台(tenant_id=0)
2. POST /system/app
- owner_type=PLATFORM
- platform=wechat
- tenant_id=0
3. 删缓存:DEL app:creds:wechat:0
4. 所有未自配的租户下次登录都会拉到这条
6. 缓存策略
- Key:
app:creds:{platform}:{tenantId}(tenantId=0 表示 PLATFORM 默认) - TTL: 30 分钟(1800 秒)
- 失效时机:
SysAppCRUD 写操作完成后SysApp启/禁用操作后- 超管强制刷新(可选接口)
- 防穿透:缓存空对象 5 分钟(针对不存在的 tenantId)
- 序列化:用 fastjson2 序列化
AppCredentialsVO
7. OAuth2 端改造
7.1 改造点
| 文件 | 改动 |
|---|---|
OAuth2ServerConfig |
删 @Bean @ConfigurationProperties 声明 wechat/alipay AppProperties |
| 同上 | 注入 SysAppFeign + AppCredentialsCache |
authorizationServerFilterChain |
根据 grant_type 动态选 platform,调用 appCredentialsCache.get(platform, tenantId) 拿凭证 |
7.2 兼容与过渡
- 旧的
thirdparty.*Nacos 配置可以保留一段过渡期,但不读取 AppProperties仍可作为"应用启动时的兜底",但 OAuth2 登录链路不再使用
8. 安全考虑
- 脱敏返回:
SysAppDTO中app_secret返回******,详情接口需要*:*:app:detail权限 - 审计日志:所有 CRUD 写操作记录操作人
- 加密占位:
is_encrypted字段保留,后续接入 AES 时只改 service 层 - 租户越权防护:CRUD 接口根据当前用户 tenantId 过滤;非超管只能操作
tenant_id=current的记录
9. 边界与不做
- 不做:AES 加密实现(仅预留
is_encrypted字段) - 不做:SysApp 导入导出
- 不做:SysApp 版本控制/变更历史(仅靠
updated_by/at) - 不做:多语言 i18n(所有界面文案中文)
- 不涉及:
rui-service-user模块(user 表/用户登录管理不在本次范围)
10. 验收标准
- 超管能创建
owner_type=PLATFORM的 wechat 默认配置 - 租户能创建
owner_type=TENANT的 wechat 自配(覆盖默认) - CRUD 接口都通;
uk_tenant_owner_platform唯一约束生效 SysAppInnerController.getCredentials能被 oauth2 Feign 调用- OAuth2 登录时凭证从缓存读,CRUD 后缓存被清
- 不存在的 tenantId 第二次调用不会再打 DB(空对象缓存)
- 编译通过 21 个模块 BUILD SUCCESS