diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/pom.xml b/daxpay-platform/daxpay-platform-capability/capability-alipay/pom.xml
index 94af025f5..49af4bcb2 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-alipay/pom.xml
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/pom.xml
@@ -24,11 +24,22 @@
daxpay-platform-core
${project.version}
-
+
- com.alipay.sdk
- alipay-sdk-java
- ${alipay-sdk.version}
+ cn.hutool
+ hutool-http
+ ${hutool.version}
+
+
+
+ cn.hutool
+ hutool-json
+ ${hutool.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/AlipayAutoConfiguration.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/AlipayAutoConfiguration.java
index 9f6aed1c7..d4d0fb8e5 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/AlipayAutoConfiguration.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/AlipayAutoConfiguration.java
@@ -6,8 +6,8 @@ import org.springframework.context.annotation.ComponentScan;
/// # 支付宝开放平台能力模块自动配置
///
-/// 装配支付宝 OAuth 认证服务([AlipayAuthCapability]), 供 iam 模块(授权登录)
-/// 与 payment 模块(通道认证)共用, 不耦合配置存储。
+/// 装配支付宝 OAuth 认证服务([AlipayAuthCapability], OpenAPI 直连无官方 SDK),
+/// 供 iam 模块(授权登录)与 payment 模块(通道认证)共用, 不耦合配置存储。
///
@Slf4j
@AutoConfiguration
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/config/AlipayAuthTypeEnum.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/config/AlipayAuthTypeEnum.java
index 73243b00c..3fcb910b4 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/config/AlipayAuthTypeEnum.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/config/AlipayAuthTypeEnum.java
@@ -5,9 +5,9 @@ import lombok.Getter;
/// # 支付宝鉴权方式
///
-/// 区分公钥模式与证书模式, 决定支付宝 SDK 的接口调用方式:
-/// - [KEY] 公钥模式: 使用 alipayPublicKey, 调用 `AlipayClient.execute`
-/// - [CERT] 证书模式: 使用三本证书, 调用 `AlipayClient.certificateExecute`
+/// 区分公钥模式与证书模式, 决定 OpenAPI 请求参数与响应验签公钥来源:
+/// - [KEY] 公钥模式: 使用 alipayPublicKey 验签
+/// - [CERT] 证书模式: 请求附加 app_cert_sn / alipay_root_cert_sn, 从支付宝公钥证书取验签公钥
///
@Getter
@AllArgsConstructor
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/service/AlipayAuthCapability.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/service/AlipayAuthCapability.java
index a21c11553..9ed2ddedb 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/service/AlipayAuthCapability.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/auth/service/AlipayAuthCapability.java
@@ -3,12 +3,8 @@ package cn.daxpay.open.platform.capability.alipay.auth.service;
import cn.daxpay.open.platform.capability.alipay.auth.config.AlipayAuthConfig;
import cn.daxpay.open.platform.capability.alipay.auth.config.AlipayAuthTypeEnum;
import cn.daxpay.open.platform.capability.alipay.auth.result.AlipayAuthResult;
+import cn.daxpay.open.platform.capability.alipay.openapi.AlipayOpenApiClient;
import cn.daxpay.open.platform.core.exception.operation.OperationFailException;
-import com.alipay.api.AlipayClient;
-import com.alipay.api.AlipayConfig;
-import com.alipay.api.DefaultAlipayClient;
-import com.alipay.api.request.AlipaySystemOauthTokenRequest;
-import com.alipay.api.response.AlipaySystemOauthTokenResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -22,8 +18,8 @@ import java.util.Objects;
/// 从各自配置来源装载 [AlipayAuthConfig] 后传入本服务。
///
/// ## 能力边界
-/// - 生成授权链接(纯字符串拼接, 不需要 SDK)
-/// - authCode 换 userId/openId/accessToken(调用 `alipay.system.oauth.token`)
+/// - 生成授权链接(纯字符串拼接)
+/// - authCode 换 userId/openId/accessToken(直连 `alipay.system.oauth.token`, 无官方 SDK)
/// - 不做 state/queryCode 缓存(由调用方自行管理 Redis 会话)
///
@Slf4j
@@ -59,69 +55,25 @@ public class AlipayAuthCapability {
/// 通过 authCode 换取用户标识
///
- /// 调用支付宝 `alipay.system.oauth.token` 接口(grant_type=authorization_code),
+ /// 直连支付宝 `alipay.system.oauth.token`(grant_type=authorization_code),
/// 返回 userId(传统) / openId(新标准) / accessToken(可用于后续 user.info.share)。
+ /// 平台级配置固定生产网关。
///
/// @param config 支付宝开放平台配置(完整凭据)
/// @param authCode 支付宝回调回传的 auth_code
/// @return 含 userId / openId / accessToken 的授权结果
public AlipayAuthResult getUserId(AlipayAuthConfig config, String authCode) {
- AlipayClient client = this.buildClient(config);
- AlipaySystemOauthTokenRequest request = new AlipaySystemOauthTokenRequest();
- request.setCode(authCode);
- request.setGrantType("authorization_code");
try {
- AlipayAuthTypeEnum authType = AlipayAuthTypeEnum.fromCode(config.getAuthType());
- AlipaySystemOauthTokenResponse response = authType.isCert()
- ? client.certificateExecute(request)
- : client.execute(request);
- if (!response.isSuccess()) {
- log.warn("支付宝换取用户标识失败, code={}, msg={}, subMsg={}",
- response.getCode(), response.getMsg(), response.getSubMsg());
- // 支付宝: 换取用户标识失败: {0}
- throw new OperationFailException("error.alipay.authFailed", response.getSubMsg());
- }
- return new AlipayAuthResult()
- .setUserId(response.getUserId())
- .setOpenId(response.getOpenId())
- .setAccessToken(response.getAccessToken());
+ return AlipayOpenApiClient.oauthToken(config, authCode, false);
} catch (OperationFailException e) {
throw e;
} catch (Exception e) {
log.error("支付宝换取用户标识异常", e);
- // 支付宝: 换取用户标识异常: {0}
+ // 支付宝: 换取用户标识失败: {0}
throw new OperationFailException("error.alipay.authFailed", e.getMessage());
}
}
- /// 根据 [AlipayAuthConfig] 构造 [AlipayClient]
- ///
- /// - 公钥模式: appId + privateKey + alipayPublicKey
- /// - 证书模式: appId + privateKey + 三本证书内容
- private AlipayClient buildClient(AlipayAuthConfig config) {
- try {
- AlipayConfig alipayConfig = new AlipayConfig();
- alipayConfig.setAppId(config.getAppId());
- AlipayAuthTypeEnum authType = AlipayAuthTypeEnum.fromCode(config.getAuthType());
- if (authType.isCert()) {
- // 证书模式
- alipayConfig.setPrivateKey(config.getPrivateKey());
- alipayConfig.setAppCertContent(config.getAppCert());
- alipayConfig.setAlipayPublicCertContent(config.getAlipayCert());
- alipayConfig.setRootCertContent(config.getAlipayRootCert());
- } else {
- // 公钥模式
- alipayConfig.setPrivateKey(config.getPrivateKey());
- alipayConfig.setAlipayPublicKey(config.getAlipayPublicKey());
- }
- return new DefaultAlipayClient(alipayConfig);
- } catch (Exception e) {
- log.error("支付宝 AlipayClient 构造失败", e);
- // 支付宝: SDK 客户端构造失败: {0}
- throw new OperationFailException("error.alipay.clientInitFailed", e.getMessage());
- }
- }
-
/// 判断配置是否完整可用(appId + privateKey 必填, 公钥/证书按 authType 二选一)
public boolean isConfigured(AlipayAuthConfig config) {
if (config == null) {
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayCertUtil.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayCertUtil.java
new file mode 100644
index 000000000..92e1a6dd0
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayCertUtil.java
@@ -0,0 +1,113 @@
+package cn.daxpay.open.platform.capability.alipay.openapi;
+
+import lombok.experimental.UtilityClass;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.List;
+
+/// # 支付宝证书工具
+///
+/// 对齐官方 SDK `AntCertificationUtil`:
+/// - app_cert_sn: MD5(issuerDN + serialNumber) 补齐 32 位十六进制
+/// - alipay_root_cert_sn: 根证书链中 RSA 签名算法(OID 1.2.840.113549.1.1.*) 证书 SN, 多值 `_` 连接
+/// - 从支付宝公钥证书提取 Base64 编码的 X509 公钥
+///
+@UtilityClass
+public class AlipayCertUtil {
+
+ /// RSA 签名算法 OID 前缀
+ private static final String RSA_SIG_ALG_OID_PREFIX = "1.2.840.113549.1.1";
+
+ static {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ /// 计算应用公钥证书 SN(`app_cert_sn`)
+ public String getAppCertSn(String appCertContent) {
+ X509Certificate cert = parseCertificate(appCertContent);
+ return getCertSn(cert);
+ }
+
+ /// 计算支付宝根证书 SN(`alipay_root_cert_sn`)
+ public String getRootCertSn(String rootCertContent) {
+ X509Certificate[] certs = parseCertificateChain(rootCertContent);
+ StringBuilder rootCertSn = null;
+ for (X509Certificate cert : certs) {
+ if (cert.getSigAlgOID() != null && cert.getSigAlgOID().startsWith(RSA_SIG_ALG_OID_PREFIX)) {
+ String certSn = getCertSn(cert);
+ if (rootCertSn == null || (rootCertSn.isEmpty())) {
+ rootCertSn = certSn == null ? null : new StringBuilder(certSn);
+ } else {
+ rootCertSn.append("_").append(certSn);
+ }
+ }
+ }
+ return rootCertSn == null ? null : rootCertSn.toString();
+ }
+
+ /// 从支付宝公钥证书提取 Base64 公钥(X509 编码)
+ public String getAlipayPublicKeyFromCert(String alipayCertContent) {
+ X509Certificate cert = parseCertificate(alipayCertContent);
+ PublicKey publicKey = cert.getPublicKey();
+ return Base64.getEncoder().encodeToString(publicKey.getEncoded());
+ }
+
+ /// 计算单本证书 SN
+ public String getCertSn(X509Certificate cert) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ // 与官方 SDK 一致: 使用平台默认字符集对 issuerDN+serial 做 digest
+ md.update((cert.getIssuerX500Principal().getName() + cert.getSerialNumber()).getBytes());
+ return fillMd5(new BigInteger(1, md.digest()).toString(16));
+ } catch (Exception e) {
+ throw new IllegalStateException("alipay cert sn failed: " + e.getMessage(), e);
+ }
+ }
+
+ /// 解析单本证书
+ public X509Certificate parseCertificate(String certContent) {
+ try {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);
+ try (ByteArrayInputStream input = new ByteArrayInputStream(certContent.getBytes(StandardCharsets.UTF_8))) {
+ return (X509Certificate) factory.generateCertificate(input);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("alipay cert parse failed: " + e.getMessage(), e);
+ }
+ }
+
+ /// 解析证书链(根证书可能含多本)
+ public X509Certificate[] parseCertificateChain(String certContent) {
+ try {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);
+ try (ByteArrayInputStream input = new ByteArrayInputStream(certContent.getBytes(StandardCharsets.UTF_8))) {
+ Collection extends Certificate> certificates = factory.generateCertificates(input);
+ List list = new ArrayList<>(certificates.size());
+ for (Certificate certificate : certificates) {
+ list.add((X509Certificate) certificate);
+ }
+ return list.toArray(new X509Certificate[0]);
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("alipay cert chain parse failed: " + e.getMessage(), e);
+ }
+ }
+
+ private String fillMd5(String md5) {
+ return md5.length() == 32 ? md5 : fillMd5("0" + md5);
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiClient.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiClient.java
new file mode 100644
index 000000000..4e739d83c
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiClient.java
@@ -0,0 +1,243 @@
+package cn.daxpay.open.platform.capability.alipay.openapi;
+
+import cn.daxpay.open.platform.capability.alipay.auth.config.AlipayAuthConfig;
+import cn.daxpay.open.platform.capability.alipay.auth.config.AlipayAuthTypeEnum;
+import cn.daxpay.open.platform.capability.alipay.auth.result.AlipayAuthResult;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+/// # 支付宝开放平台 OpenAPI 客户端
+///
+/// 仅实现 `alipay.system.oauth.token`(auth_code 换 userId/openId/accessToken),
+/// 不依赖官方 SDK。支持公钥模式与证书模式。
+///
+@Slf4j
+@UtilityClass
+public class AlipayOpenApiClient {
+
+ private static final String GATEWAY_PRODUCTION = "https://openapi.alipay.com/gateway.do";
+
+ private static final String GATEWAY_SANDBOX = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
+
+ private static final String METHOD_OAUTH_TOKEN = "alipay.system.oauth.token";
+
+ private static final String RESPONSE_NODE = "alipay_system_oauth_token_response";
+
+ private static final String ERROR_NODE = "error_response";
+
+ private static final DateTimeFormatter TIMESTAMP_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private static final int HTTP_TIMEOUT_MS = 15_000;
+
+ /// 调用 `alipay.system.oauth.token` 换取用户标识
+ ///
+ /// @param config 支付宝凭据
+ /// @param authCode 授权码
+ /// @param sandbox 是否沙箱网关
+ public AlipayAuthResult oauthToken(AlipayAuthConfig config, String authCode, boolean sandbox) {
+ Map params = buildCommonParams(config);
+ params.put("grant_type", "authorization_code");
+ params.put("code", authCode);
+ // 证书模式附加 SN
+ if (AlipayAuthTypeEnum.fromCode(config.getAuthType()).isCert()) {
+ params.put("app_cert_sn", AlipayCertUtil.getAppCertSn(config.getAppCert()));
+ params.put("alipay_root_cert_sn", AlipayCertUtil.getRootCertSn(config.getAlipayRootCert()));
+ }
+ String signContent = AlipayOpenApiSigner.getSignContent(params);
+ params.put("sign", AlipayOpenApiSigner.rsa2Sign(signContent, config.getPrivateKey()));
+
+ String gateway = sandbox ? GATEWAY_SANDBOX : GATEWAY_PRODUCTION;
+ String body;
+ try (HttpResponse response = HttpRequest.post(gateway)
+ .form(new HashMap<>(params))
+ .timeout(HTTP_TIMEOUT_MS)
+ .execute()) {
+ body = response.body();
+ } catch (Exception e) {
+ log.error("支付宝 OpenAPI 请求失败, method={}", METHOD_OAUTH_TOKEN, e);
+ throw new IllegalStateException("alipay openapi request failed: " + e.getMessage(), e);
+ }
+ if (body == null || body.isBlank()) {
+ throw new IllegalStateException("alipay openapi empty response");
+ }
+ // 验签
+ verifyResponse(body, config);
+ // 解析业务节点
+ JSONObject root = JSONUtil.parseObj(body);
+ JSONObject biz = root.getJSONObject(RESPONSE_NODE);
+ if (biz == null) {
+ biz = root.getJSONObject(ERROR_NODE);
+ }
+ if (biz == null) {
+ throw new IllegalStateException("alipay openapi missing response node");
+ }
+ String code = biz.getStr("code");
+ if (code != null && !code.isBlank() && !"10000".equals(code)) {
+ String subMsg = biz.getStr("sub_msg");
+ String msg = subMsg != null && !subMsg.isBlank() ? subMsg : biz.getStr("msg");
+ throw new IllegalStateException(msg != null ? msg : code);
+ }
+ return new AlipayAuthResult()
+ .setUserId(biz.getStr("user_id"))
+ .setOpenId(biz.getStr("open_id"))
+ .setAccessToken(biz.getStr("access_token"));
+ }
+
+ /// 组装公共请求参数
+ private Map buildCommonParams(AlipayAuthConfig config) {
+ Map params = new HashMap<>();
+ params.put("app_id", config.getAppId());
+ params.put("method", METHOD_OAUTH_TOKEN);
+ params.put("format", "JSON");
+ params.put("charset", "UTF-8");
+ params.put("sign_type", "RSA2");
+ params.put("timestamp", LocalDateTime.now().format(TIMESTAMP_FMT));
+ params.put("version", "1.0");
+ return params;
+ }
+
+ /// 响应验签: 截取 response 节点原文 + RSA2
+ private void verifyResponse(String body, AlipayAuthConfig config) {
+ String sign = extractJsonStringField(body, "sign");
+ if (sign == null || sign.isBlank()) {
+ // 部分错误响应可能无 sign, 交给业务解析抛错
+ return;
+ }
+ String source = extractSignSource(body);
+ if (source == null || source.isBlank()) {
+ throw new IllegalStateException("alipay response sign source empty");
+ }
+ String publicKey = resolveVerifyPublicKey(config);
+ if (!AlipayOpenApiSigner.rsa2Verify(source, sign, publicKey)) {
+ throw new IllegalStateException("alipay response sign verify failed");
+ }
+ }
+
+ /// 公钥模式用配置公钥; 证书模式从支付宝公钥证书提取
+ private String resolveVerifyPublicKey(AlipayAuthConfig config) {
+ if (AlipayAuthTypeEnum.fromCode(config.getAuthType()).isCert()) {
+ return AlipayCertUtil.getAlipayPublicKeyFromCert(config.getAlipayCert());
+ }
+ return config.getAlipayPublicKey();
+ }
+
+ /// 截取 `alipay_system_oauth_token_response` 或 `error_response` 节点的 JSON 原文
+ private String extractSignSource(String body) {
+ int index = body.indexOf(RESPONSE_NODE);
+ String node = RESPONSE_NODE;
+ if (index < 0) {
+ index = body.indexOf(ERROR_NODE);
+ node = ERROR_NODE;
+ }
+ if (index < 0) {
+ return null;
+ }
+ // 节点名后的 `":` 再往后找 `{` 或 `"`
+ int startSearch = index + node.length() + 2;
+ return extractSignContent(body, startSearch);
+ }
+
+ /// 对齐官方 `AlipaySignature.extractSignContent`: 从 begin 起找到 `{...}` 或 `"..."` 片段
+ private String extractSignContent(String responseString, int begin) {
+ int beginIndex = begin;
+ while (beginIndex < responseString.length()
+ && responseString.charAt(beginIndex) != '{'
+ && responseString.charAt(beginIndex) != '"') {
+ beginIndex++;
+ }
+ if (beginIndex >= responseString.length()) {
+ return null;
+ }
+ int endIndex;
+ if (responseString.charAt(beginIndex) == '{') {
+ endIndex = extractJsonObjectEndPosition(responseString, beginIndex);
+ } else {
+ endIndex = extractJsonStringEndPosition(responseString, beginIndex);
+ }
+ return responseString.substring(beginIndex, endIndex);
+ }
+
+ private int extractJsonObjectEndPosition(String responseString, int beginPosition) {
+ LinkedList braces = new LinkedList<>();
+ boolean inQuotes = false;
+ int consecutiveEscapeCount = 0;
+ for (int index = beginPosition; index < responseString.length(); index++) {
+ char currentChar = responseString.charAt(index);
+ if (currentChar == '"' && consecutiveEscapeCount % 2 == 0) {
+ inQuotes = !inQuotes;
+ } else if (currentChar == '{' && !inQuotes) {
+ braces.push('{');
+ } else if (currentChar == '}' && !inQuotes) {
+ braces.pop();
+ if (braces.isEmpty()) {
+ return index + 1;
+ }
+ }
+ if (currentChar == '\\') {
+ consecutiveEscapeCount++;
+ } else {
+ consecutiveEscapeCount = 0;
+ }
+ }
+ return responseString.length();
+ }
+
+ private int extractJsonStringEndPosition(String responseString, int beginPosition) {
+ for (int index = beginPosition; index < responseString.length(); index++) {
+ if (responseString.charAt(index) == '"' && index != beginPosition) {
+ return index + 1;
+ }
+ }
+ return responseString.length();
+ }
+
+ /// 粗提取顶层 JSON 字符串字段(用于 sign)
+ private String extractJsonStringField(String body, String field) {
+ String pattern = "\"" + field + "\"";
+ int idx = body.lastIndexOf(pattern);
+ if (idx < 0) {
+ return null;
+ }
+ int colon = body.indexOf(':', idx + pattern.length());
+ if (colon < 0) {
+ return null;
+ }
+ int start = colon + 1;
+ while (start < body.length() && Character.isWhitespace(body.charAt(start))) {
+ start++;
+ }
+ if (start >= body.length() || body.charAt(start) != '"') {
+ return null;
+ }
+ start++;
+ StringBuilder sb = new StringBuilder();
+ boolean escape = false;
+ for (int i = start; i < body.length(); i++) {
+ char c = body.charAt(i);
+ if (escape) {
+ sb.append(c);
+ escape = false;
+ continue;
+ }
+ if (c == '\\') {
+ escape = true;
+ continue;
+ }
+ if (c == '"') {
+ return sb.toString();
+ }
+ sb.append(c);
+ }
+ return null;
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiSigner.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiSigner.java
new file mode 100644
index 000000000..359304c9e
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiSigner.java
@@ -0,0 +1,110 @@
+package cn.daxpay.open.platform.capability.alipay.openapi;
+
+import lombok.experimental.UtilityClass;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/// # 支付宝开放平台 RSA2 签名/验签
+///
+/// 对齐官方 SDK `AlipaySignature` 的 RSA2 行为:
+/// - 签名串: 除 sign 外非空参数按 key 字典序 `k=v&k=v`
+/// - 算法: SHA256withRSA + Base64
+/// - 密钥: 支持 PEM 与裸 Base64(PKCS8 私钥 / X509 公钥)
+/// - 字符集固定 UTF-8(避免平台默认编码差异)
+///
+@UtilityClass
+public class AlipayOpenApiSigner {
+
+ private static final String SIGN_ALGORITHM = "SHA256withRSA";
+
+ /// 将参数拼成待签名字符串(排除 sign, 跳过空值, key 字典序)
+ public String getSignContent(Map params) {
+ List keys = new ArrayList<>(params.keySet());
+ Collections.sort(keys);
+ StringBuilder content = new StringBuilder();
+ int index = 0;
+ for (String key : keys) {
+ if ("sign".equals(key)) {
+ continue;
+ }
+ String value = params.get(key);
+ if (key == null || key.isEmpty() || value == null || value.isEmpty()) {
+ continue;
+ }
+ if (index > 0) {
+ content.append('&');
+ }
+ content.append(key).append('=').append(value);
+ index++;
+ }
+ return content.toString();
+ }
+
+ /// RSA2 私钥签名, 返回 Base64
+ public String rsa2Sign(String content, String privateKeyContent) {
+ try {
+ PrivateKey privateKey = loadPrivateKey(privateKeyContent);
+ Signature signature = Signature.getInstance(SIGN_ALGORITHM);
+ signature.initSign(privateKey);
+ signature.update(content.getBytes(StandardCharsets.UTF_8));
+ return Base64.getEncoder().encodeToString(signature.sign());
+ } catch (Exception e) {
+ throw new IllegalStateException("alipay rsa2 sign failed: " + e.getMessage(), e);
+ }
+ }
+
+ /// RSA2 公钥验签
+ public boolean rsa2Verify(String content, String sign, String publicKeyContent) {
+ try {
+ PublicKey publicKey = loadPublicKey(publicKeyContent);
+ Signature signature = Signature.getInstance(SIGN_ALGORITHM);
+ signature.initVerify(publicKey);
+ signature.update(content.getBytes(StandardCharsets.UTF_8));
+ return signature.verify(Base64.getDecoder().decode(sign));
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /// 加载 PKCS8 私钥(兼容 PEM 头与裸 Base64)
+ public PrivateKey loadPrivateKey(String keyContent) {
+ try {
+ byte[] decoded = Base64.getDecoder().decode(stripKey(keyContent));
+ return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
+ } catch (Exception e) {
+ throw new IllegalStateException("alipay private key parse failed: " + e.getMessage(), e);
+ }
+ }
+
+ /// 加载 X509 公钥(兼容 PEM 头与裸 Base64)
+ public PublicKey loadPublicKey(String keyContent) {
+ try {
+ byte[] decoded = Base64.getDecoder().decode(stripKey(keyContent));
+ return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
+ } catch (Exception e) {
+ throw new IllegalStateException("alipay public key parse failed: " + e.getMessage(), e);
+ }
+ }
+
+ private String stripKey(String keyContent) {
+ return keyContent
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replace("-----BEGIN RSA PRIVATE KEY-----", "")
+ .replace("-----END RSA PRIVATE KEY-----", "")
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replaceAll("\\s", "");
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/en-US/error/alipay.json b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/en-US/error/alipay.json
index 30eb46450..aa9bb4fcb 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/en-US/error/alipay.json
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/en-US/error/alipay.json
@@ -1,4 +1,4 @@
{
"authFailed": "Failed to get Alipay user identifier: {0}",
- "clientInitFailed": "Failed to init Alipay SDK client: {0}"
+ "clientInitFailed": "Failed to init Alipay OpenAPI client: {0}"
}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/zh-CN/error/alipay.json b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/zh-CN/error/alipay.json
index d44727a0c..5e88fa9ef 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/zh-CN/error/alipay.json
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/main/resources/i18n/zh-CN/error/alipay.json
@@ -1,4 +1,4 @@
{
"authFailed": "换取支付宝用户标识失败: {0}",
- "clientInitFailed": "支付宝 SDK 客户端构造失败: {0}"
+ "clientInitFailed": "支付宝 OpenAPI 初始化失败: {0}"
}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-alipay/src/test/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiSignerTest.java b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/test/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiSignerTest.java
new file mode 100644
index 000000000..2e8c7602c
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-capability/capability-alipay/src/test/java/cn/daxpay/open/platform/capability/alipay/openapi/AlipayOpenApiSignerTest.java
@@ -0,0 +1,45 @@
+package cn.daxpay.open.platform.capability.alipay.openapi;
+
+import org.junit.jupiter.api.Test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/// 支付宝 OpenAPI 签名工具单测
+class AlipayOpenApiSignerTest {
+
+ @Test
+ void getSignContent_sortsAndSkipsEmpty() {
+ Map params = new LinkedHashMap<>();
+ params.put("method", "alipay.system.oauth.token");
+ params.put("app_id", "2021000000000000");
+ params.put("sign", "should-skip");
+ params.put("code", "authcode");
+ params.put("empty", "");
+ params.put("charset", "UTF-8");
+
+ String content = AlipayOpenApiSigner.getSignContent(params);
+ assertEquals(
+ "app_id=2021000000000000&charset=UTF-8&code=authcode&method=alipay.system.oauth.token",
+ content);
+ }
+
+ @Test
+ void rsa2SignAndVerify_roundTrip() throws Exception {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ KeyPair keyPair = generator.generateKeyPair();
+ String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
+ String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
+
+ String content = "app_id=2021&method=alipay.system.oauth.token";
+ String sign = AlipayOpenApiSigner.rsa2Sign(content, privateKey);
+ assertTrue(AlipayOpenApiSigner.rsa2Verify(content, sign, publicKey));
+ }
+}
diff --git a/pom.xml b/pom.xml
index fa0f74725..8157c380b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,8 +56,6 @@
3.3.7
4.8.4.B
0.24.0
-
- 4.40.272.ALL