评论

微信支付如何使用国密?

微信支付如何使用国密,附 Java 源码

[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,让支付触手可及。

开源地址 https://gitee.com/javen205/IJPay

最后一次编辑于  2023-11-15  
点赞 1
收藏
评论
登录 后发表内容