docs(plan): wechat/alipay Provider 凭证动态加载改造计划
修复凭证烧死 bug:把 WeixinAuthenticationProvider / AlipayAuthenticationProvider 改为持有 AppCredentialsCache, buildToken 内部按 X-App-Id 头动态解析凭证并构造 API 客户端。 - 任务 1: WeixinAuthenticationProvider 改造 - 任务 2: AlipayAuthenticationProvider 改造 - 任务 3: OAuth2ServerConfig 清理 - 任务 4: AlipayApiClient 字段映射核对
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
# 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 (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 修复 `WeixinAuthenticationProvider` / `AlipayAuthenticationProvider` 的凭证烧死 bug —— 改为持有 `AppCredentialsCache`、在请求时按 `X-App-Id` 动态解析凭证,使 SysApp CRUD / 缓存过期能立即生效。
|
||||
|
||||
**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
|
||||
|
||||
### 验收点
|
||||
- [ ] 微信登录请求:第一次请求时 `WechatApiClient` 用 `X-App-Id` 解析的凭证调微信 API
|
||||
- [ ] SysApp 增删改后:缓存被 evict,下次请求自动用新凭证(不需重启)
|
||||
- [ ] 缓存过期 30min 后:下次请求自动从 DB 重新加载凭证
|
||||
- [ ] `X-App-Id` 缺失 / 凭证不存在:抛 `OAuth2AuthenticationException` + `server_error` + 描述含 appId
|
||||
- [ ] 编译通过 `rui-common-oauth2` 模块
|
||||
- [ ] 不影响 `PasswordAuthenticationProvider` / `SmsAuthenticationProvider`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: WeixinAuthenticationProvider 改造
|
||||
|
||||
**Files:**
|
||||
- Modify: `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/authentication/weixin/WeixinAuthenticationProvider.java`
|
||||
|
||||
- [ ] **Step 1: 替换字段**
|
||||
|
||||
把 `private final WechatApiClient wechatApiClient;` 改为:
|
||||
```java
|
||||
private final AppCredentialsCache appCredentialsCache;
|
||||
```
|
||||
|
||||
- [ ] **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;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **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 不变。
|
||||
|
||||
- [ ] **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";
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 编译验证**
|
||||
|
||||
```bash
|
||||
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
|
||||
```
|
||||
|
||||
预期:`BUILD SUCCESS`
|
||||
|
||||
- [ ] **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`
|
||||
|
||||
- [ ] **Step 1: 替换字段**
|
||||
|
||||
把 `private final AlipayApiClient alipayApiClient;` 改为:
|
||||
```java
|
||||
private final AppCredentialsCache appCredentialsCache;
|
||||
```
|
||||
|
||||
- [ ] **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;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 添加 `X-App-Id` 读取辅助方法**(同 Task 1 Step 3)
|
||||
|
||||
- [ ] **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 客户端
|
||||
AlipayApiClient alipayApiClient = new AlipayApiClient(
|
||||
creds.getAppId(),
|
||||
creds.getAppKey(),
|
||||
creds.getAesKey() == null ? "" : creds.getAesKey());
|
||||
|
||||
// 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)。
|
||||
|
||||
- [ ] **Step 5: 编译验证**
|
||||
|
||||
```bash
|
||||
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
|
||||
```
|
||||
|
||||
- [ ] **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`
|
||||
|
||||
- [ ] **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);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 删除 `resolveCredentials` / `currentRequestAppId` 私有方法**
|
||||
|
||||
删除约 151-181 行的两个方法。
|
||||
|
||||
- [ ] **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;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编译验证**
|
||||
|
||||
```bash
|
||||
mvn -pl rui-common/rui-common-oauth2 -am compile -DskipTests
|
||||
```
|
||||
|
||||
预期:`BUILD SUCCESS`
|
||||
|
||||
- [ ] **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`
|
||||
|
||||
- [ ] **Step 1: 核对构造签名**
|
||||
|
||||
读取两个文件,确认:
|
||||
- `AlipayApiClient` 构造参数类型与 `AppCredentialsVO` 提供的 getter 一一对应
|
||||
- 如字段名不一致(如 `privateKey` vs `appSecret`),调整 Task 2 的代码
|
||||
|
||||
- [ ] **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** - 在本会话中执行任务,批量执行并设置检查点
|
||||
|
||||
**请选择执行方式?**
|
||||
Reference in New Issue
Block a user