[TOC]
我们知道微信支付提供了 V2 以及V3 版本的接口,V2 版本支持MD5以及HMAC-SHA256,V3 版本支持RSA算法。微信支付在2023.04.28更新了文档V3版本支持了国密
1、国密介绍
那什么是国密呢?
为什么要使用国密呢?
如何申请使用国密呢?
这个三个问题官方已给出了答案 https://pay.weixin.qq.com/docs/merchant/development/shangmi/introduction.html
没错,目前使用国密需要申请哈
2、获取国家商用密码证书和密钥
如何获取国家商用密码证书和密钥呢?
官方也提供了详细的操作文档,按照文档操作即可
https://pay.weixin.qq.com/docs/merchant/development/shangmi/key-and-certificate.html
3、APIv3接口如何使用国家商用密码
经过一顿操作后,不出意外就会拿到 PEM 格式的国密证书(序列号_证书算法版本),但程序中如何使用呢?
官方也给了一个简单的流程说明可以参考
https://pay.weixin.qq.com/docs/merchant/development/shangmi/guide.html
官方是推荐直接使用 微信支付官方Java SDK 但我们不少企业有依赖管理的要求。
这就引出了一个问题,我们自己如何实现国密呢?
4、实现微信的国密支付
获取国密平台证书为例,代码如下
4.1 实现生成国密签名
核心代码如下,完整源码请参考文末源码链接。国密请求签名的规则,同国际算法基本一致,仅更改了具体算法。国际算法可参考 签名生成
/**
* 签名
*
* @param privateKey 私钥
* @param content 需要签名的内容
* @return 返回结果
* @throws Exception 异常信息
*/
public static String sm2SignWithSm3(PrivateKey privateKey, String content) throws Exception {
// 生成SM2sign with sm3 签名验签算法实例
Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString()
, new BouncyCastleProvider());
// 使用私钥签名,初始化签名实例
signature.initSign(privateKey);
// 签名原文
byte[] plainText = content.getBytes(StandardCharsets.UTF_8);
// 写入签名原文到算法中
signature.update(plainText);
// 计算签名值
byte[] signatureValue = signature.sign();
return Base64.encode(signatureValue);
}
/**
* 获取商户私钥
*
* @param keyPath 商户私钥证书路径
* @return {@link PrivateKey} 商户私钥
* @throws Exception 异常信息
*/
public static PrivateKey getPrivateKey(String keyPath, String authType) throws Exception {
String originalKey = getCertFileContent(keyPath);
if (StrUtil.isEmpty(originalKey)) {
throw new RuntimeException("商户私钥证书获取失败");
}
return getPrivateKeyByKeyContent(originalKey, authType);
}
/**
* 通过路径获取证书文件的内容
*
* @param path 文件路径
* @return 文件内容
*/
public static String getCertFileContent(String path) throws IOException {
InputStream certFileInputStream = getCertFileInputStream(path);
return IoUtil.read(certFileInputStream, StandardCharsets.UTF_8);
}
/**
* 通过路径获取证书文件的输入流
*
* @param path 文件路径
* @return 文件流
* @throws IOException 异常信息
*/
public static InputStream getCertFileInputStream(String path) throws IOException {
if (StrUtil.isBlank(path)) {
return null;
}
// 绝对地址
File file = new File(path);
if (file.exists()) {
return Files.newInputStream(file.toPath());
}
// 相对地址
return getFileToStream(path);
}
/**
* 传入 classPath 静态资源路径返回文件输入流
*
* @param classPath 静态资源路径
* @return InputStream
*/
public static InputStream getFileToStream(String classPath) {
Resource resource = new ClassPathResource(classPath);
return resource.getStream();
}
/**
* 获取国密证书私钥
*
* @param privateKey 私钥
* @return 返回值
* @throws Exception 异常信息
*/
public static PrivateKey getSmPrivateKey(String privateKey) throws Exception {
byte[] encPrivate = Base64.decode(privateKey);
return getSmPrivateKey(encPrivate);
}
/**
* 获取国密证书私钥
*
* @param encPrivate 私钥
* @return 返回值
* @throws Exception 异常信息
*/
public static PrivateKey getSmPrivateKey(byte[] encPrivate) throws Exception {
KeyFactory keyFact = KeyFactory.getInstance("EC", new BouncyCastleProvider());
return keyFact.generatePrivate(new PKCS8EncodedKeySpec(encPrivate));
}
4.2 下载平台证书返回内容解密
对下载平台证书接口返回的内容进行解密,解密后的内容即为平台公钥证书。
核心代码如下,完整源码请参考文末源码链接。
/**
* 下载平台证书解密
*
* @param key3 APIv3密钥
* @param cipherText 密文
* @param nonce 随机串
* @param associatedData 附加数据
* @return 解密后的明文
* @throws Exception 异常信息
*/
public static String sm4DecryptToString(String key3, String cipherText, String nonce, String associatedData) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding", new BouncyCastleProvider());
byte[] keyByte = PayKit.sm3Hash(key3);
SecretKeySpec key = new SecretKeySpec(keyByte, "SM4");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8);
}
/**
* SM3 Hash
*
* @param content 原始内容
* @return 返回结果
* @throws Exception 异常信息
*/
public static byte[] sm3Hash(String content) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SM3", new BouncyCastleProvider());
byte[] contentDigest = digest.digest(content.getBytes(StandardCharsets.UTF_8));
return Arrays.copyOf(contentDigest, 16);
}
4.3 如何验证国密签名
核心代码如下,完整源码请参考文末源码链接。
/**
* 验证签名
*
* @param response 接口请求返回的 {@link IJPayHttpResponse}
* @param certPath 平台证书路径
* @return 签名结果
* @throws Exception 异常信息
*/
public static boolean verifySignature(IJPayHttpResponse response, String certPath) throws Exception {
String timestamp = response.getHeader("Wechatpay-Timestamp");
String nonceStr = response.getHeader("Wechatpay-Nonce");
String signature = response.getHeader("Wechatpay-Signature");
String signatureType = response.getHeader("Wechatpay-Signature-Type");
String body = response.getBody();
System.out.println("timestamp:" + timestamp);
System.out.println("nonceStr:" + nonceStr);
System.out.println("signature:" + signature);
System.out.println("signatureType:" + signatureType);
System.out.println("body:" + body);
return verifySignature(signatureType, signature, body, nonceStr, timestamp, PayKit.getCertFileInputStream(certPath));
}
/**
* 验证签名
*
* @param signatureType 签名类型
* @param signature 待验证的签名
* @param body 应答主体
* @param nonce 随机串
* @param timestamp 时间戳
* @param certInputStream 微信支付平台证书输入流
* @return 签名结果
* @throws Exception 异常信息
*/
public static boolean verifySignature(String signatureType, String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception {
String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body);
// 获取证书
X509Certificate certificate = PayKit.getCertificate(certInputStream);
PublicKey publicKey = certificate.getPublicKey();
if (StrUtil.equals(signatureType, AuthTypeEnum.SM2.getCode())) {
return PayKit.sm4Verify(publicKey, buildSignMessage, signature);
}
return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey);
}
/**
* 国密验签
*
* @param publicKey 平台证书公钥
* @param data 待验签的签名原文
* @param originalSignature 签名值
* @return 验签结果
* @throws Exception 异常信息
*/
public static boolean sm4Verify(PublicKey publicKey, String data, String originalSignature) throws Exception {
Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString()
, new BouncyCastleProvider());
signature.initVerify(publicKey);
// 写入待验签的签名原文到算法中
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.decode(originalSignature.getBytes(StandardCharsets.UTF_8)));
}
4.4 完整示例代码
@RequestMapping("/get")
@ResponseBody
public String v3Get() {
// 获取平台证书列表
try {
IJPayHttpResponse response = WxPayApi.v3(
RequestMethodEnum.GET,
WxDomainEnum.CHINA.toString(),
CertAlgorithmTypeEnum.getCertSuffixUrl(CertAlgorithmTypeEnum.SM2.getCode()),
wxPayV3Bean.getMchId(),
getSerialNumber(),
null,
wxPayV3Bean.getKeyPath(),
"",
AuthTypeEnum.SM2.getCode()
);
Map<String, List<String>> headers = response.getHeaders();
log.info("请求头: {}", headers);
String timestamp = response.getHeader("Wechatpay-Timestamp");
String nonceStr = response.getHeader("Wechatpay-Nonce");
String serialNumber = response.getHeader("Wechatpay-Serial");
String signature = response.getHeader("Wechatpay-Signature");
String body = response.getBody();
int status = response.getStatus();
log.info("serialNumber: {}", serialNumber);
log.info("status: {}", status);
log.info("body: {}", body);
int isOk = 200;
if (status == isOk) {
JSONObject jsonObject = JSONUtil.parseObj(body);
JSONArray dataArray = jsonObject.getJSONArray("data");
// 默认认为只有一个平台证书
JSONObject encryptObject = dataArray.getJSONObject(0);
JSONObject encryptCertificate = encryptObject.getJSONObject("encrypt_certificate");
String associatedData = encryptCertificate.getStr("associated_data");
String cipherText = encryptCertificate.getStr("ciphertext");
String nonce = encryptCertificate.getStr("nonce");
String algorithm = encryptCertificate.getStr("algorithm");
String serialNo = encryptObject.getStr("serial_no");
final String platSerialNo = savePlatformCert(associatedData, nonce, cipherText, algorithm, wxPayV3Bean.getPlatformCertPath());
log.info("平台证书序列号: {} serialNo: {}", platSerialNo, serialNo);
// 根据证书序列号查询对应的证书来验证签名结果
boolean verifySignature = WxPayKit.verifySignature(response, wxPayV3Bean.getPlatformCertPath());
log.info("verifySignature:{}", verifySignature);
}
return body;
} catch (Exception e) {
log.error("获取平台证书列表异常", e);
return null;
}
}
5、开源项目
IJPay 聚合支付SDK,让支付触手可及。