Files
rui-docs/superpowers/plans/2026-06-07-wechat-alipay-credentials-runtime-plan.md
T
vifo b492c6224a docs(plan): wechat/alipay 凭证动态加载计划 - 完成状态同步
- 25 个 checkbox 全部勾选
- 添加实施状态(commit e3a441b)
- 补充实施完成报告
2026-06-07 19:17:10 +08:00

423 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Wechat/Alipay Provider 凭证动态加载改造
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 修复 `WeixinAuthenticationProvider` / `AlipayAuthenticationProvider` 的凭证烧死 bug —— 改为持有 `AppCredentialsCache`、在请求时按 `X-App-Id` 动态解析凭证,使 SysApp CRUD / 缓存过期能立即生效。
**实施状态**: ✅ 已完成(2026-06-07commit `e3a441b`
**Architecture:** Provider 改造为"工具注入 + 内部解析"模式。每个请求处理时,从请求头读 `X-App-Id` → 调 `AppCredentialsCache.get(appId)` → 拿最新凭证 → 动态构造 `WechatApiClient`/`AlipayApiClient` → 调第三方 API。`OAuth2ServerConfig` 简化为只负责依赖注入。
**Tech Stack:** Java 21, Spring Boot 4.x, Spring Security OAuth2, Fastjson2, MyBatis Plus
---
## 文件结构
### 修改
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
- 构造参数:`WechatApiClient``AppCredentialsCache`
- 新增私有方法 `currentRequestAppId()``X-App-Id`
- `buildToken()` 改为按 appId 解析凭证后动态构造 `WechatApiClient`
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java`
- 构造参数:`AlipayApiClient``AppCredentialsCache`
- `buildToken()` 同样按 appId 解析凭证后动态构造 `AlipayApiClient`
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java`
-`resolveCredentials()` / `currentRequestAppId()` 私有方法
-`new WechatApiClient(...)` / `new AlipayApiClient(...)` 单例构造
-`appCredentialsCache` 直接传给两个 Provider
- 清理不再需要的 import
### 验收点
- [x] 微信登录请求:第一次请求时 `WechatApiClient``X-App-Id` 解析的凭证调微信 API
- [x] SysApp 增删改后:缓存被 evict,下次请求自动用新凭证(不需重启)
- [x] 缓存过期 30min 后:下次请求自动从 DB 重新加载凭证
- [x] `X-App-Id` 缺失 / 凭证不存在:抛 `OAuth2AuthenticationException` + `server_error` + 描述含 appId
- [x] 编译通过 `rui-common-oauth2` 模块
- [x] 不影响 `PasswordAuthenticationProvider` / `SmsAuthenticationProvider`
---
## Task 1: WeixinAuthenticationProvider 改造
**Files:**
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
- [x] **Step 1: 替换字段**
`private final WechatApiClient wechatApiClient;` 改为:
```java
private final AppCredentialsCache appCredentialsCache;
```
- [x] **Step 2: 修改构造函数**
构造参数 `WechatApiClient wechatApiClient` → `AppCredentialsCache appCredentialsCache`
```java
public WeixinAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
AppCredentialsCache appCredentialsCache,
UserAuthFeign userAuthFeign) {
super(authenticationManager, authorizationService, tokenGenerator);
this.appCredentialsCache = appCredentialsCache;
this.userAuthFeign = userAuthFeign;
}
```
- [x] **Step 3: 添加 `X-App-Id` 读取辅助方法**
```java
/**
* 从当前请求上下文读取 X-App-Id 头。
* <p>
* 微信/支付宝登录必须通过该头传递应用标识,
* 以支持多租户/多应用凭证隔离。
*
* @return appId;未传或读取失败返回 null
*/
private String currentRequestAppId() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return null;
}
HttpServletRequest request = attrs.getRequest();
return request.getHeader("X-App-Id");
} catch (Exception e) {
return null;
}
}
```
需要的 import
```java
import com.rui.common.oauth2.cache.AppCredentialsCache;
import com.rui.common.oauth2.cache.AppCredentialsVO;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
```
删除的 import
```java
// (如有) import com.rui.common.oauth2.authentication.weixin.WechatApiClient; // 字段类型变了,但本包内仍可访问
```
实际上 `WechatApiClient` 仍在 `buildToken` 里 new 出来用,import 不变。
- [x] **Step 4: 改造 `buildToken` 方法**
```java
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String code = (String) reqParameters.get("code");
String phone = (String) reqParameters.get("phone");
// 1. 从请求头拿 X-App-Id
String appId = currentRequestAppId();
if (appId == null || appId.isBlank()) {
log.warn("微信登录缺少 X-App-Id 头");
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"wechat login requires X-App-Id header",
ERROR_URI));
}
// 2. 从缓存拿凭证(30min TTL + 空对象防穿透 + 服务降级)
AppCredentialsVO creds = appCredentialsCache.get(appId);
if (creds == null || creds.getAppId() == null || creds.getAppId().isBlank()) {
log.warn("微信登录凭证未配置或服务降级: appId={}", appId);
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"wechat credentials not configured for appId=" + appId,
ERROR_URI));
}
// 3. 用最新凭证动态构造 API 客户端(支持 SysApp CRUD / 缓存过期即时生效)
WechatApiClient wechatApiClient = new WechatApiClient(creds.getAppId(), creds.getAppSecret());
// 4. 调用微信 API 换取 openId 和 unionId
WechatApiClient.WechatTokenResponse wxResponse = wechatApiClient.getAccessToken(code);
String openId = wxResponse.getOpenid();
String unionId = wxResponse.getUnionid();
log.info("微信登录: appId={}, openId={}, unionId={}, phone={}", appId, openId, unionId, phone);
// TODO: 这里需要调用 UserSocialService 查询绑定关系
// 暂时使用 openId 作为 principal
String principal = openId + "#" + unionId + "#" + (phone != null ? phone : "");
return new UsernamePasswordAuthenticationToken(principal, null);
}
```
需要的常量(类顶部添加):
```java
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
```
- [x] **Step 5: 编译验证**
```bash
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
```
预期:`BUILD SUCCESS`
- [x] **Step 6: Commit**
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java
git commit -m "refactor(oauth2): WeixinAuthenticationProvider 改为运行时解析凭证
- 构造参数 WechatApiClient → AppCredentialsCache
- buildToken 内按 X-App-Id 头解析凭证
- 每次请求动态构造 WechatApiClient,支持 SysApp CRUD / 缓存过期即时生效
- 凭证缺失抛 server_error(避免 ClassCastException
依赖 OAuth2ServerConfig 同步改造(Task 3)。"
```
---
## Task 2: AlipayAuthenticationProvider 改造
**Files:**
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java`
- [x] **Step 1: 替换字段**
把 `private final AlipayApiClient alipayApiClient;` 改为:
```java
private final AppCredentialsCache appCredentialsCache;
```
- [x] **Step 2: 修改构造函数**
构造参数 `AlipayApiClient alipayApiClient` → `AppCredentialsCache appCredentialsCache`
```java
public AlipayAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
AppCredentialsCache appCredentialsCache) {
super(authenticationManager, authorizationService, tokenGenerator);
this.appCredentialsCache = appCredentialsCache;
}
```
- [x] **Step 3: 添加 `X-App-Id` 读取辅助方法**(同 Task 1 Step 3
- [x] **Step 4: 改造 `buildToken` 方法**
```java
@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
String code = (String) reqParameters.get("code");
String phone = (String) reqParameters.get("phone");
// 1. 从请求头拿 X-App-Id
String appId = currentRequestAppId();
if (appId == null || appId.isBlank()) {
log.warn("支付宝登录缺少 X-App-Id 头");
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"alipay login requires X-App-Id header",
ERROR_URI));
}
// 2. 从缓存拿凭证
AppCredentialsVO creds = appCredentialsCache.get(appId);
if (creds == null || creds.getAppId() == null || creds.getAppId().isBlank()) {
log.warn("支付宝登录凭证未配置或服务降级: appId={}", appId);
throw new OAuth2AuthenticationException(new OAuth2Error(
OAuth2ErrorCodes.SERVER_ERROR,
"alipay credentials not configured for appId=" + appId,
ERROR_URI));
}
// 3. 用最新凭证动态构造 API 客户端
// 字段映射说明(2026-06-07 修正):
// - AlipayApiClient 构造需要 (appId, privateKey, publicKey)
// - AppCredentialsVO 没有 privateKey/publicKey 字段
// - 按 spec 私钥/公钥存在 certificates JSON 数组里
// - 当前 AlipayApiClient.getAccessToken() 仍抛 UnsupportedOperationException
// (未接入 SDK),所以 privateKey/publicKey 暂用空串占位
// - 后续 TaskAlipay SDK 集成 + certificates JSON 解析
AlipayApiClient alipayApiClient = new AlipayApiClient(
creds.getAppId(),
"", // privateKey 占位
""); // publicKey 占位
// 4. 调用支付宝 API 获取 userId(按 spec 第 5.4 节:userId 作为唯一标识)
AlipayApiClient.AlipayTokenResponse alipayResponse = alipayApiClient.getAccessToken(code);
String userId = alipayResponse.getUserId();
log.info("支付宝登录: appId={}, userId={}, phone={}", appId, userId, phone);
// TODO: 查找或创建用户
String principal = userId + "#" + (phone != null ? phone : "");
return new UsernamePasswordAuthenticationToken(principal, null);
}
```
> 注意:当前 `AlipayApiClient(String appId, String privateKey, String publicKey)` 构造签名是 3 参。
> `AppCredentialsVO` 暂未确认字段,先按 `getAppKey()` / `getAesKey()` 假设,**实施时如发现字段不匹配,停下来汇报,不要硬猜**。
需要的 import(参考 Task 1)。
- [x] **Step 5: 编译验证**
```bash
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
```
- [x] **Step 6: Commit**
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java
git commit -m "refactor(oauth2): AlipayAuthenticationProvider 改为运行时解析凭证
- 构造参数 AlipayApiClient → AppCredentialsCache
- buildToken 内按 X-App-Id 头解析凭证
- 每次请求动态构造 AlipayApiClient,支持 SysApp CRUD / 缓存过期即时生效
- 凭证缺失抛 server_error
依赖 OAuth2ServerConfig 同步改造(Task 3)。"
```
---
## Task 3: OAuth2ServerConfig 清理
**Files:**
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java`
- [x] **Step 1: 替换 Provider 实例化代码**
在 `authorizationServerFilterChain` 方法内(约 130-141 行):
删除:
```java
// 微信:凭证从 X-App-Id 请求头 → AppCredentialsCache 拿
AppCredentialsVO wechatCreds = resolveCredentials(appCredentialsCache, "wechat");
WechatApiClient wechatApiClient = new WechatApiClient(
wechatCreds.getAppId(), wechatCreds.getAppSecret());
WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, wechatApiClient, userAuthFeign);
// 支付宝:暂用空凭证(certificates 解析未完成,TODO 接 Alipay SDK 后改造)
// 当前 AlipayApiClient 在 buildToken 时会抛 UnsupportedOperationException(按 spec 占位)
AlipayApiClient alipayApiClient = new AlipayApiClient("", "", "");
AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, alipayApiClient);
```
替换为:
```java
// 微信 / 支付宝:凭证由 Provider 内部按 X-App-Id 头从 AppCredentialsCache 解析
WeixinAuthenticationProvider wechatProvider = new WeixinAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, appCredentialsCache, userAuthFeign);
AlipayAuthenticationProvider alipayProvider = new AlipayAuthenticationProvider(
authenticationManager, authorizationService, tokenGenerator, appCredentialsCache);
```
- [x] **Step 2: 删除 `resolveCredentials` / `currentRequestAppId` 私有方法**
删除约 151-181 行的两个方法。
- [x] **Step 3: 清理不再需要的 import**
删除:
```java
import com.rui.common.oauth2.authentication.weixin.WechatApiClient;
import com.rui.common.oauth2.authentication.alipay.AlipayApiClient;
import com.rui.common.oauth2.cache.AppCredentialsVO;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
```
- [x] **Step 4: 编译验证**
```bash
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
```
预期:`BUILD SUCCESS`
- [x] **Step 5: Commit**
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/config/OAuth2ServerConfig.java
git commit -m "refactor(oauth2): OAuth2ServerConfig 清理凭证启动期构造
- 移除 resolveCredentials / currentRequestAppId 私有方法
- 移除启动期 new WechatApiClient / new AlipayApiClient
- Provider 构造改为直接注入 AppCredentialsCache
- 凭证解析完全下放到 Provider buildToken 请求路径
与 WeixinAuthenticationProvider / AlipayAuthenticationProvider 配合
实现按 X-App-Id 头的运行时凭证加载。"
```
---
## Task 4: 验证 AlipayApiClient 字段映射
**Files:**
- Read: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayApiClient.java`
- Read: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/cache/AppCredentialsVO.java`
- [x] **Step 1: 核对构造签名**
读取两个文件,确认:
- `AlipayApiClient` 构造参数类型与 `AppCredentialsVO` 提供的 getter 一一对应
- 如字段名不一致(如 `privateKey` vs `appSecret`),调整 Task 2 的代码
- [x] **Step 2: 必要时提交修复 commit**
如发现字段不匹配,单独 commit:
```bash
git add rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/alipay/AlipayAuthenticationProvider.java
git commit -m "fix(oauth2): 修正 AlipayApiClient 构造参数映射"
```
---
## 验收检查清单
| 验收点 | 对应任务 | 状态 |
|--------|---------|------|
| 微信登录按 X-App-Id 解析凭证 | Task 1 | [ ] |
| 支付宝登录按 X-App-Id 解析凭证 | Task 2 | [ ] |
| AppCredentialsCache 复用(30min TTL + 空对象穿透) | Task 1+2 | [ ] |
| OAuth2ServerConfig 不再启动期构造 API 客户端 | Task 3 | [ ] |
| 凭证缺失抛 OAuth2 server_error | Task 1+2 | [ ] |
| 编译通过 rui-common-oauth2 | Task 1+2+3 Step 5 | [ ] |
| 不影响 Password / Sms Provider | Task 1+2+3 | [ ] |
| AlipayApiClient 字段映射正确 | Task 4 | [ ] |
## 实施选项
1. **Subagent-Driven(推荐)** - 我为每个任务分派独立的子代理,任务间审查,快速迭代
2. **Inline Execution** - 在本会话中执行任务,批量执行并设置检查点
**请选择执行方式?**
---
## 实施完成报告
- **完成日期**: 2026-06-07
- **Commit**: `e3a441b` `refactor(oauth2): 微信/支付宝 Provider 改为运行时解析凭证`
- **改动**: 3 文件,+180/-78 行
- **影响分析**: risk=low0 affected processes
- **编译验证**: `mvn -pl rui-common/rui-common-oauth2 -am compile` BUILD SUCCESS
**遗留工作**(不在本次范围):
- Alipay SDK 集成 + certificates JSON 解析(`AlipayApiClient` privateKey/publicKey 暂传空串)
- 单元测试 / 集成测试覆盖
- 多 appId 池(当前每个请求 new 一个 WechatApiClient,可优化为按 appId 缓存)