package cn.lili.modules.payment.kit.core.kit; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.lili.common.enums.ResultCode; import cn.lili.common.exception.ServiceException; import cn.lili.modules.payment.kit.core.PaymentHttpResponse; import cn.lili.modules.payment.kit.core.enums.RequestMethodEnums; import cn.lili.modules.payment.kit.core.enums.SignType; import java.io.BufferedInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** *

微信支付工具类

* * @author */ public class WxPayKit { private static final String FIELD_SIGN = "sign"; private static final String FIELD_SIGN_TYPE = "sign_type"; public static String hmacSha256(String data, String key) { return PayKit.hmacSha256(data, key); } public static String md5(String data) { return PayKit.md5(data); } /** * AES 解密 * * @param base64Data 需要解密的数据 * @param key 密钥 * @return 解密后的数据 */ public static String decryptData(String base64Data, String key) { return PayKit.decryptData(base64Data, key); } /** * AES 加密 * * @param data 需要加密的数据 * @param key 密钥 * @return 加密后的数据 */ public static String encryptData(String data, String key) { return PayKit.encryptData(data, key); } public static String generateStr() { return IdUtil.fastSimpleUUID(); } /** * 支付异步通知时校验 sign * * @param params 参数 * @param partnerKey 支付密钥 * @return {boolean} */ public static boolean verifyNotify(Map params, String partnerKey) { String sign = params.get("sign"); String localSign = createSign(params, partnerKey, SignType.MD5); return sign.equals(localSign); } /** * 支付异步通知时校验 sign * * @param params 参数 * @param partnerKey 支付密钥 * @param signType {@link SignType} * @return {@link Boolean} 验证签名结果 */ public static boolean verifyNotify(Map params, String partnerKey, SignType signType) { String sign = params.get("sign"); String localSign = createSign(params, partnerKey, signType); return sign.equals(localSign); } /** * 生成签名 * * @param params 需要签名的参数 * @param partnerKey 密钥 * @param signType 签名类型 * @return 签名后的数据 */ public static String createSign(Map params, String partnerKey, SignType signType) { if (signType == null) { signType = SignType.MD5; } //生成签名前先去除sign params.remove(FIELD_SIGN); String tempStr = PayKit.createLinkString(params); String stringSignTemp = tempStr + "&key=" + partnerKey; if (signType == SignType.MD5) { return md5(stringSignTemp).toUpperCase(); } else { return hmacSha256(stringSignTemp, partnerKey).toUpperCase(); } } /** * APP 单独生成签名 * app 支付环境中,如果遇到签名错误,百思不得其解,则可以使用这个方法调用签名尝试解决 * * @param params 需要签名的参数 * @return 签名后的数据 */ public static String createAppSign(Map params, String privateKey) { String appid = params.get("appid"); String timestamp = params.get("timestamp"); String noncestr = params.get("noncestr"); String prepayid = params.get("prepayid"); String encrypt = appid + "\n" + timestamp + "\n" + noncestr + "\n" + prepayid + "\n"; try { return PayKit.createSign(encrypt, privateKey); } catch (Exception e) { throw new ServiceException(ResultCode.ERROR); } } /** * 生成签名 * * @param params 需要签名的参数 * @param secret 企业微信支付应用secret * @return 签名后的数据 */ public static String createSign(Map params, String secret) { //生成签名前先去除sign params.remove(FIELD_SIGN); String tempStr = PayKit.createLinkString(params); String stringSignTemp = tempStr + "&secret=" + secret; return md5(stringSignTemp).toUpperCase(); } /** * 构建签名 * * @param params 需要签名的参数 * @param partnerKey 密钥 * @param signType 签名类型 * @return 签名后的 Map */ public static Map buildSign(Map params, String partnerKey, SignType signType) { return buildSign(params, partnerKey, signType, true); } /** * 构建签名 * * @param params 需要签名的参数 * @param partnerKey 密钥 * @param signType 签名类型 * @param haveSignType 签名是否包含 sign_type 字段 * @return 签名后的 Map */ public static Map buildSign(Map params, String partnerKey, SignType signType, boolean haveSignType) { if (haveSignType) { params.put(FIELD_SIGN_TYPE, signType.getType()); } String sign = createSign(params, partnerKey, signType); params.put(FIELD_SIGN, sign); return params; } public static StringBuffer forEachMap(Map params, String prefix, String suffix) { return PayKit.forEachMap(params, prefix, suffix); } /** * 微信下单 map to xml * * @param params Map 参数 * @return xml 字符串 */ public static String toXml(Map params) { return PayKit.toXml(params); } /** * 针对支付的 xml,没有嵌套节点的简单处理 * * @param xmlStr xml 字符串 * @return 转化后的 Map */ public static Map xmlToMap(String xmlStr) { return PayKit.xmlToMap(xmlStr); } /** *

生成二维码链接

*

原生支付接口模式一(扫码模式一)

* * @param sign 签名 * @param appId 公众账号ID * @param mchId 商户号 * @param productId 商品ID * @param timeStamp 时间戳 * @param nonceStr 随机字符串 * @return {String} */ public static String bizPayUrl(String sign, String appId, String mchId, String productId, String timeStamp, String nonceStr) { String rules = "weixin://wxpay/bizpayurl?sign=Temp&appid=Temp&mch_id=Temp&product_id=Temp&time_stamp=Temp&nonce_str=Temp"; return replace(rules, "Temp", sign, appId, mchId, productId, timeStamp, nonceStr); } /** *

生成二维码链接

*

原生支付接口模式一(扫码模式一)

* * @param partnerKey 密钥 * @param appId 公众账号ID * @param mchId 商户号 * @param productId 商品ID * @param timeStamp 时间戳 * @param nonceStr 随机字符串 * @param signType 签名类型 * @return {String} */ public static String bizPayUrl(String partnerKey, String appId, String mchId, String productId, String timeStamp, String nonceStr, SignType signType) { HashMap map = new HashMap<>(5); map.put("appid", appId); map.put("mch_id", mchId); map.put("time_stamp", StrUtil.isEmpty(timeStamp) ? Long.toString(System.currentTimeMillis() / 1000) : timeStamp); map.put("nonce_str", StrUtil.isEmpty(nonceStr) ? IdUtil.fastSimpleUUID() : nonceStr); map.put("product_id", productId); return bizPayUrl(createSign(map, partnerKey, signType), appId, mchId, productId, timeStamp, nonceStr); } /** *

生成二维码链接

*

原生支付接口模式一(扫码模式一)

* * @param partnerKey 密钥 * @param appId 公众账号ID * @param mchId 商户号 * @param productId 商品ID * @return {String} */ public static String bizPayUrl(String partnerKey, String appId, String mchId, String productId) { String timeStamp = Long.toString(System.currentTimeMillis() / 1000); String nonceStr = IdUtil.fastSimpleUUID(); HashMap map = new HashMap<>(5); map.put("appid", appId); map.put("mch_id", mchId); map.put("time_stamp", timeStamp); map.put("nonce_str", nonceStr); map.put("product_id", productId); return bizPayUrl(createSign(map, partnerKey, null), appId, mchId, productId, timeStamp, nonceStr); } /** * 替换url中的参数 * * @param str 原始字符串 * @param regex 表达式 * @param args 替换字符串 * @return {String} */ public static String replace(String str, String regex, String... args) { for (String arg : args) { str = str.replaceFirst(regex, arg); } return str; } /** * 判断接口返回的 code * * @param codeValue code 值 * @return 是否是 SUCCESS */ public static boolean codeIsOk(String codeValue) { return StrUtil.isNotEmpty(codeValue) && "SUCCESS".equals(codeValue); } /** *

公众号支付-预付订单再次签名

*

注意此处签名方式需与统一下单的签名类型一致

* * @param prepayId 预付订单号 * @param appId 应用编号 * @param partnerKey API Key * @param signType 签名方式 * @return 再次签名后的 Map */ public static Map prepayIdCreateSign(String prepayId, String appId, String partnerKey, SignType signType) { Map packageParams = new HashMap<>(6); packageParams.put("appId", appId); packageParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); packageParams.put("nonceStr", String.valueOf(System.currentTimeMillis())); packageParams.put("package", "prepay_id=" + prepayId); if (signType == null) { signType = SignType.MD5; } packageParams.put("signType", signType.getType()); String packageSign = WxPayKit.createSign(packageParams, partnerKey, signType); packageParams.put("paySign", packageSign); return packageParams; } /** * JS 调起支付签名 * * @param appId 应用编号 * @param prepayId 预付订单号 * @param keyPath key.pem 证书路径 * @return 唤起支付需要的参数 * @throws Exception 错误信息 */ public static Map jsApiCreateSign(String appId, String prepayId, String keyPath) throws Exception { String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = String.valueOf(System.currentTimeMillis()); String packageStr = "prepay_id=" + prepayId; Map packageParams = new HashMap<>(6); packageParams.put("appId", appId); packageParams.put("timeStamp", timeStamp); packageParams.put("nonceStr", nonceStr); packageParams.put("package", packageStr); packageParams.put("signType", SignType.RSA.toString()); ArrayList list = new ArrayList<>(); list.add(appId); list.add(timeStamp); list.add(nonceStr); list.add(packageStr); String packageSign = PayKit.createSign( PayKit.buildSignMessage(list), keyPath ); packageParams.put("paySign", packageSign); return packageParams; } /** *

APP 支付-预付订单再次签名

*

注意此处签名方式需与统一下单的签名类型一致

* * @param appId 应用编号 * @param partnerId 商户号 * @param prepayId 预付订单号 * @param partnerKey API Key * @param signType 签名方式 * @return 再次签名后的 Map */ public static Map appPrepayIdCreateSign(String appId, String partnerId, String prepayId, String partnerKey, SignType signType) { Map packageParams = new HashMap<>(8); packageParams.put("appid", appId); packageParams.put("partnerid", partnerId); packageParams.put("prepayid", prepayId); packageParams.put("package", "Sign=WXPay"); packageParams.put("noncestr", String.valueOf(System.currentTimeMillis())); packageParams.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); if (signType == null) { signType = SignType.MD5; } String packageSign = createSign(packageParams, partnerKey, signType); // 部分微信APP支付 提示签名错误 解开下方注释 替换上边的代码就好。 // String packageSign = createAppSign(packageParams, partnerKey); packageParams.put("sign", packageSign); return packageParams; } /** * App 调起支付签名 * * @param appId 应用编号 * @param partnerId 商户编号 * @param prepayId 预付订单号 * @param keyPath key.pem 证书路径 * @return 唤起支付需要的参数 * @throws Exception 错误信息 */ public static Map appCreateSign(String appId, String partnerId, String prepayId, String keyPath) throws Exception { String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = String.valueOf(System.currentTimeMillis()); Map packageParams = new HashMap<>(8); packageParams.put("appId", appId); packageParams.put("partnerid", partnerId); packageParams.put("prepayid", prepayId); packageParams.put("package", "Sign=WXPay"); packageParams.put("timeStamp", timeStamp); packageParams.put("nonceStr", nonceStr); packageParams.put("signType", SignType.RSA.toString()); ArrayList list = new ArrayList<>(); list.add(appId); list.add(timeStamp); list.add(nonceStr); list.add(prepayId); String packageSign = PayKit.createSign( PayKit.buildSignMessage(list), keyPath ); packageParams.put("sign", packageSign); return packageParams; } /** *

小程序-预付订单再次签名

*

注意此处签名方式需与统一下单的签名类型一致

* * @param appId 应用编号 * @param prepayId 预付订单号 * @param partnerKey API Key * @param signType 签名方式 * @return 再次签名后的 Map */ public static Map miniAppPrepayIdCreateSign(String appId, String prepayId, String partnerKey, SignType signType) { Map packageParams = new HashMap<>(6); packageParams.put("appId", appId); packageParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); packageParams.put("nonceStr", String.valueOf(System.currentTimeMillis())); packageParams.put("package", "prepay_id=" + prepayId); if (signType == null) { signType = SignType.MD5; } packageParams.put("signType", signType.getType()); String packageSign = createSign(packageParams, partnerKey, signType); packageParams.put("paySign", packageSign); return packageParams; } /** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethodEnums} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethodEnums method, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType) throws Exception { //构建签名参数 String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); String signature = PayKit.createSign(buildSignMessage, keyPath); //根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethodEnums} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param privateKey 商户私钥 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethodEnums method, String urlSuffix, String mchId, String serialNo, PrivateKey privateKey, String body, String nonceStr, long timestamp, String authType) throws Exception { //构建签名参数 String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); String signature = PayKit.createSign(buildSignMessage, privateKey); //根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethodEnums} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethodEnums method, String urlSuffix, String mchId, String serialNo, String keyPath, String body) throws Exception { long timestamp = System.currentTimeMillis() / 1000; String authType = "WECHATPAY2-SHA256-RSA2048"; String nonceStr = IdUtil.fastSimpleUUID(); return buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); } /** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethodEnums} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param privateKey key.pem 证书路径 * @param body 接口请求参数 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethodEnums method, String urlSuffix, String mchId, String serialNo, PrivateKey privateKey, String body) throws Exception { long timestamp = System.currentTimeMillis() / 1000; String authType = "WECHATPAY2-SHA256-RSA2048"; String nonceStr = IdUtil.fastSimpleUUID(); return buildAuthorization(method, urlSuffix, mchId, serialNo, privateKey, body, nonceStr, timestamp, authType); } /** * 验证签名 * * @param response 接口请求返回的 {@link PaymentHttpResponse} * @param certPath 平台证书路径 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(PaymentHttpResponse response, String certPath) throws Exception { String timestamp = response.getHeader("Wechatpay-Timestamp"); String nonceStr = response.getHeader("Wechatpay-Nonce"); String signature = response.getHeader("Wechatpay-Signature"); String body = response.getBody(); return verifySignature(signature, body, nonceStr, timestamp, FileUtil.getInputStream(certPath)); } /** * 验证签名 * * @param response 接口请求返回的 {@link PaymentHttpResponse} * @param cert 平台证书 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(PaymentHttpResponse response, X509Certificate cert) throws Exception { String timestamp = response.getHeader("Wechatpay-Timestamp"); String nonceStr = response.getHeader("Wechatpay-Nonce"); String signature = response.getHeader("Wechatpay-Signature"); String body = response.getBody(); return verifySignature(signature, body, nonceStr, timestamp, cert); } /** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param publicKey 微信支付平台公钥 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, String publicKey) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param publicKey {@link PublicKey} 微信支付平台公钥 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, PublicKey publicKey) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param certInputStream 微信支付平台证书输入流 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(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(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param certificate 微信支付平台证书 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, X509Certificate certificate) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); PublicKey publicKey = certificate.getPublicKey(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * v3 支付异步通知验证签名 * * @param serialNo 证书序列号 * @param body 异步通知密文 * @param signature 签名 * @param nonce 随机字符串 * @param timestamp 时间戳 * @param key api 密钥 * @param certPath 平台证书路径 * @return 异步通知明文 * @throws Exception 异常信息 */ public static String verifyNotify(String serialNo, String body, String signature, String nonce, String timestamp, String key, String certPath) throws Exception { BufferedInputStream inputStream = FileUtil.getInputStream(certPath); //获取平台证书序列号 X509Certificate certificate = PayKit.getCertificate(inputStream); String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); System.out.println(serialNumber); //验证证书序列号 if (serialNumber.equals(serialNo)) { boolean verifySignature = WxPayKit.verifySignature(signature, body, nonce, timestamp, certificate.getPublicKey()); if (verifySignature) { JSONObject resultObject = JSONUtil.parseObj(body); JSONObject resource = resultObject.getJSONObject("resource"); String cipherText = resource.getStr("ciphertext"); String nonceStr = resource.getStr("nonce"); String associatedData = resource.getStr("associated_data"); AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8)); //密文解密 return aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonceStr.getBytes(StandardCharsets.UTF_8), cipherText ); } } return null; } /** * v3 支付异步通知验证签名 * * @param serialNo 证书序列号 * @param body 异步通知密文 * @param signature 签名 * @param nonce 随机字符串 * @param timestamp 时间戳 * @param key api 密钥 * @return 异步通知明文 * @throws Exception 异常信息 */ public static String verifyNotify(String serialNo, String body, String signature, String nonce, String timestamp, String key, X509Certificate certificate) throws Exception { String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); //验证证书序列号 if (serialNumber.equals(serialNo)) { boolean verifySignature = WxPayKit.verifySignature(signature, body, nonce, timestamp, certificate.getPublicKey()); if (verifySignature) { JSONObject resultObject = JSONUtil.parseObj(body); JSONObject resource = resultObject.getJSONObject("resource"); String cipherText = resource.getStr("ciphertext"); String nonceStr = resource.getStr("nonce"); String associatedData = resource.getStr("associated_data"); AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8)); //密文解密 return aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonceStr.getBytes(StandardCharsets.UTF_8), cipherText ); } } return null; } }