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 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