mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 15:35:38 +08:00
refactor(capability-alipay): 移除官方 SDK,直连 oauth.token
capability-alipay 仅用于认证登录/换 openId,改用 HTTP + RSA2 自实现 alipay.system.oauth.token,避免主应用引入约 32MB 的 alipay-sdk-java。
This commit is contained in:
@@ -24,11 +24,22 @@
|
||||
<artifactId>daxpay-platform-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<!-- 支付宝开放平台 SDK -->
|
||||
<!-- hutool http (OpenAPI 直连) -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>${alipay-sdk.version}</version>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-http</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
<!-- hutool json (响应解析) -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-json</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -6,8 +6,8 @@ import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/// # 支付宝开放平台能力模块自动配置
|
||||
///
|
||||
/// 装配支付宝 OAuth 认证服务([AlipayAuthCapability]), 供 iam 模块(授权登录)
|
||||
/// 与 payment 模块(通道认证)共用, 不耦合配置存储。
|
||||
/// 装配支付宝 OAuth 认证服务([AlipayAuthCapability], OpenAPI 直连无官方 SDK),
|
||||
/// 供 iam 模块(授权登录)与 payment 模块(通道认证)共用, 不耦合配置存储。
|
||||
///
|
||||
@Slf4j
|
||||
@AutoConfiguration
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<X509Certificate> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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<String, String> buildCommonParams(AlipayAuthConfig config) {
|
||||
Map<String, String> 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<Character> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> params) {
|
||||
List<String> 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", "");
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"authFailed": "换取支付宝用户标识失败: {0}",
|
||||
"clientInitFailed": "支付宝 SDK 客户端构造失败: {0}"
|
||||
"clientInitFailed": "支付宝 OpenAPI 初始化失败: {0}"
|
||||
}
|
||||
|
||||
@@ -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<String, String> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user