package com.nq.utils.pay;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
/**
* Pay v2 网关数字签名(与官方文档一致)。
*
* 数据拼接 stringA:集合 M 内参数按参数名 ASCII 字典序排序;格式 {@code key1=value1&key2=value2};
* 值为空不参与;不做 URL Encode;{@code sign} 不参与拼接。
*
* 数据签名:对 stringA 使用商户 PKCS8 私钥做 {@code SHA256withRSA},再 Base64 得到 {@code sign}。
*
* 数据验签:用平台公钥校验。若报文含 {@code data} 对象,则对 {@code data} 内除 {@code sign} 外的字段拼 stringA 再验签(一般为接口响应);
* 否则对顶层除 {@code sign} 外的字段验签(一般为异步通知)。
*
* 密钥:2048 位、PKCS8 生成可参考 https://uutool.cn/rsa-generate/ ;商户公钥发给客服录入;私钥配置在 {@link com.nq.common.PayV2GatewayKeys#MERCHANT_PRIVATE_KEY_PEM}。
*/
public final class PayV2RsaSignUtil {
private static final String RSA = "RSA";
private static final String SIGN_ALG = "SHA256withRSA";
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private PayV2RsaSignUtil() {
}
public static String buildStringA(Map params) {
Map sorted = new TreeMap<>();
for (Map.Entry e : params.entrySet()) {
String k = e.getKey();
if (k == null || "sign".equalsIgnoreCase(k)) {
continue;
}
String v = e.getValue();
if (v == null || v.isEmpty()) {
continue;
}
sorted.put(k, v);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry e : sorted.entrySet()) {
sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
return sb.toString();
}
public static String sign(String stringA, String merchantPrivateKeyPemOrBase64) throws Exception {
PrivateKey privateKey = parsePrivateKey(merchantPrivateKeyPemOrBase64);
Signature signature = Signature.getInstance(SIGN_ALG);
signature.initSign(privateKey);
signature.update(stringA.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
public static String sign(Map params, String merchantPrivateKeyPemOrBase64) throws Exception {
return sign(buildStringA(params), merchantPrivateKeyPemOrBase64);
}
public static boolean verify(String stringA, String signBase64, String platformPublicKeyPemOrBase64) {
if (signBase64 == null || signBase64.isEmpty()) {
return false;
}
try {
PublicKey publicKey = parsePublicKey(platformPublicKeyPemOrBase64);
Signature signature = Signature.getInstance(SIGN_ALG);
signature.initVerify(publicKey);
signature.update(stringA.getBytes(StandardCharsets.UTF_8));
byte[] sig = Base64.getDecoder().decode(signBase64.trim().replace("\n", "").replace("\r", ""));
return signature.verify(sig);
} catch (Exception e) {
return false;
}
}
public static boolean verify(Map params, String signBase64, String platformPublicKeyPemOrBase64) {
return verify(buildStringA(params), signBase64, platformPublicKeyPemOrBase64);
}
/**
* 解析商户 RSA 私钥:支持 PKCS#8 PEM、PKCS#1 PEM(RSA PRIVATE KEY)、或裸 PKCS#8 Base64。
*/
public static PrivateKey parsePrivateKey(String pemOrBase64) throws Exception {
String s = pemOrBase64 == null ? "" : pemOrBase64.trim();
if (s.isEmpty() || s.contains("REPLACE_WITH")) {
throw new IllegalArgumentException(
"PayV2GatewayKeys.MERCHANT_PRIVATE_KEY_PEM 未配置有效私钥:请粘贴完整 PEM(推荐 PKCS#8:-----BEGIN PRIVATE KEY-----),勿填公钥或占位符。");
}
if (looksLikeRsaPublicKeySpkiBase64(s)) {
throw new IllegalArgumentException(
"MERCHANT_PRIVATE_KEY_PEM 内容疑似「公钥」(X.509 SPKI)。签名必须使用「私钥」;公钥应只填在 MERCHANT_PUBLIC_KEY 并发给支付平台。");
}
if (s.contains("BEGIN")) {
try (PEMParser pemParser = new PEMParser(new StringReader(s))) {
Object obj = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
if (obj instanceof PEMKeyPair) {
return converter.getKeyPair((PEMKeyPair) obj).getPrivate();
}
if (obj instanceof PrivateKeyInfo) {
return converter.getPrivateKey((PrivateKeyInfo) obj);
}
}
throw new IllegalArgumentException(
"PEM 中未解析出 RSA 私钥。请确认含 -----BEGIN PRIVATE KEY----- 或 -----BEGIN RSA PRIVATE KEY----- 完整块。");
}
byte[] keyBytes = Base64.getDecoder().decode(s.replaceAll("\\s+", ""));
try {
return KeyFactory.getInstance(RSA).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException(
"裸 Base64 不是 PKCS#8 私钥 DER。请粘贴完整 PEM,或用 openssl:openssl pkcs8 -topk8 -nocrypt -in rsa.key -out pkcs8.pem", e);
}
}
/**
* 无 PEM 头时,常见 2048 位 RSA 公钥 SPKI 的 Base64 固定前缀(与 PKCS#8 私钥不同)。
*/
private static boolean looksLikeRsaPublicKeySpkiBase64(String s) {
if (s.contains("BEGIN")) {
return false;
}
String compact = s.replaceAll("\\s+", "");
return compact.startsWith("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA");
}
public static PublicKey parsePublicKey(String pemOrBase64) throws Exception {
String s = pemOrBase64.trim();
byte[] keyBytes;
if (s.contains("BEGIN PUBLIC KEY")) {
keyBytes = Base64.getDecoder().decode(extractPemBody(s, "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----"));
} else {
keyBytes = Base64.getDecoder().decode(s.replaceAll("\\s+", ""));
}
return KeyFactory.getInstance(RSA).generatePublic(new X509EncodedKeySpec(keyBytes));
}
private static String extractPemBody(String pem, String begin, String end) {
int a = pem.indexOf(begin);
if (a < 0) {
return pem.replaceAll("\\s+", "");
}
a += begin.length();
int b = pem.indexOf(end, a);
if (b < 0) {
return pem.substring(a).replaceAll("\\s+", "");
}
return pem.substring(a, b).replaceAll("\\s+", "");
}
}