直接上干货
package com.yzf.mall.services.support.wxpay.service.impl;
import static com.alipay.api.internal.util.file.FileUtils.openInputStream;
import com.google.common.collect.Maps;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.Security;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.Base64Utils;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class WxPay {
/**
* 微信退款解密信息需要借助第三方加解密工具bouncycastle,引入第三方包 https://mvnrepository.com/artifact/bouncycastle
*/
private static BouncyCastleProvider provider;
private static InputStream certStream;
// 将第三方包加载到内存
static {
provider = new BouncyCastleProvider();
Security.addProvider(provider);
try {
certStream = openInputStream(new File("微信商户号p12证书路径"));
} catch (IOException e) {
e.printStackTrace();
}
}
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓微信支付配置化参数↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
private String appId = "";
private String appSecret = "";
/**
* 商户api
*/
private String apiSecret = "商户号的密钥";
private String mchId = "商户号ID";
/**
* 商品简单描述
*/
private String body = "支付描述";
/**
* 交易类型
*/
private String signType = "MD5";
private String feeType = "CNY";
private String sceneInfo = "{\"h5_info\":{\"type\":\"online pay\",\"wap_url\":\"www.songjingzhou.com\",\"wap_name\":\"songjingzhou\"}}";
private String payNotifyUrl = "https://wxnotify.songjingzhou.com/api/notify/wxPayNotify";
private String refundNotifyUrl = "https://wxnotify.songjingzhou.com/api/notify/wxRefundNotify";
private String spbillCreateIp;
private String transferUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers";
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑微信支付配置化参数↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
public static final String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";
private static final int CONNECT_TIME_OUT = 8000;
private static final int READ_TIME_OUT = 10000;
public Map<String, String> getWxPayResponse(PayInfo payInfo) throws Exception {
HashMap<String, String> param = Maps.newHashMap();
param.put("body", body); // 描述
param.put("openid", payInfo.getOpenId());
param.put("out_trade_no", payInfo.getOid()); // 系统内部订单号,最好加前缀予以区分,否则公众号和小程序的订单无法区分
param.put("total_fee", String.valueOf(payInfo.getTotalFee()));
param.put("spbill_create_ip", spbillCreateIp);
param.put("trade_type", "JSAPI");
param.put("scene_info", sceneInfo);
param.put("notify_url", payNotifyUrl);
try {
Map<String, String> requestMap = fillRequestData(param);
String respXml = requestWithoutCert(UNIFIED_ORDER_URL, requestMap, CONNECT_TIME_OUT,
READ_TIME_OUT);
Map<String, String> res = this.processResponseXml(respXml);
// 上一步得到的结果集需要再次签名后给前端使用
return _reSign4Front(res.get("nonce_str"), res.get("prepay_id"));
} catch (Exception e) {
throw e;
}
}
private Map<String, String> _reSign4Front(String nonceStr, String prePayId)
throws Exception {
Map<String, String> param = Maps.newHashMap();
param.put("appId", appId); // 商户账号appid
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
param.put("timeStamp", timeStamp);
param.put("nonceStr", nonceStr);
param.put("package", "prepay_id=" + prePayId);
param.put("signType", "MD5");
String paySign = generateSignature(param, apiSecret, "MD5");
Map<String, String> result = Maps.newHashMap();
result.put("appId", appId);
result.put("timeStamp", timeStamp);
result.put("nonceStr", nonceStr);
result.put("prepayId", "prepay_id=" + prePayId); // 注意这不是手误
result.put("signType", "MD5");
result.put("paySign", paySign);
return result;
}
/**
* 微信支付异步通知,可以进行白名单限制
*/
public String wxPayNotify(String xmlStr) throws Exception {
try {
Map<String, String> map = processResponseXml(xmlStr);
// TODO 业务处理
} catch (Exception e) {
throw e;
}
return _setWxReturnXML();
}
private String _setWxReturnXML() {
return "<xml><return_code><![CDATA[" + "SUCCESS"
+ "]]></return_code><return_msg><![CDATA[" + "OK"
+ "]]></return_msg></xml>";
}
/**
* 微信退款
*/
public void wxPayRefund(WxRefundRequest request) throws Exception {
try {
Map<String, String> param = Maps.newHashMap();
param.put("transaction_id", request.getTransactionId());
param.put("out_refund_no", request.getPayRefundId());
param.put("total_fee", String.valueOf(request.getTotalFee()));
param.put("refund_fee", String.valueOf(request.getRefundFee()));
param.put("refund_fee_type", feeType);
param.put("refund_desc", request.getRefundDesc());
param.put("notify_url", payNotifyUrl);
Map<String, String> requestMap = fillRequestData(param);
String xml = requestWithCert(REFUND_URL, requestMap, CONNECT_TIME_OUT, READ_TIME_OUT);
Map<String, String> responseMap = processResponseXml(xml);
// TODO 业务处理
} catch (Exception e) {
throw e;
}
}
public String wxRefundNotify(String xmlStr) throws Exception {
Map<String, String> map;
try {
map = xmlToMap(xmlStr);
} catch (Exception e) {
throw e;
}
String reqInfo = map.get("req_info");
Map<String, String> notify = _decodeRefundNotify(reqInfo);
if ("SUCCESS".equals(notify.get("refund_status"))) {
// 业务主键
String id = notify.get("out_refund_no");
// TODO 业务逻辑
}
return _setWxReturnXML();
}
/**
* 退款解密在windows上没问题,但是在linux环境中会出现 JCE cannot authenticate the provider BC
*
* JDK 8 注意 linux需要 在jdk1.8.0_171/jre/lib/ext 下添加 bcprov-jdk15on-1.62.jar 同时修改
* jdk1.8.0_171/jre/lib/security文件添加 security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider
*
* JDK 11 没有jre目录,不能按照上面的方式处理,而亚马逊版的jdk11 则不需要做任何修改可以直接使用
*/
private Map<String, String> _decodeRefundNotify(String reqInfo) throws Exception {
String result;
try {
String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
String keyMd5Code = DigestUtils.md5Hex(apiSecret).toLowerCase();
SecretKeySpec key = new SecretKeySpec(keyMd5Code.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptContent = cipher.doFinal(Base64Utils.decodeFromString(reqInfo));
result = new String(decryptContent, StandardCharsets.UTF_8);
Map<String, String> map = xmlToMap(result);
return map;
} catch (Throwable t) {
throw t;
}
}
/**
* 微信转账
*/
public void wxTransfer(String partnerTradeId, String openId, String amount, String node)
throws Exception {
Map<String, String> param = Maps.newHashMap();
param.put("mch_appid", appId);
param.put("mchid", mchId);
param.put("partner_trade_no", partnerTradeId);
param.put("openid", openId);
param.put("check_name", "NO_CHECK");
param.put("amount", amount);
param.put("desc", node);
Map<String, String> requestMap = _sign4Transfer(param);
String response = requestWithCert(transferUrl, requestMap, 8000, 10000);
try {
Map<String, String> map = xmlToMap(response);
// TODO 业务
} catch (Throwable th) {
}
}
/**
* 微信转账签名
*/
private Map<String, String> _sign4Transfer(Map<String, String> reqData) throws Exception {
reqData.put("nonce_str", generateNonceStr());
reqData.put("sign", generateSignature(reqData, apiSecret, signType));
return reqData;
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(Map<String, String> data, String key,
String signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals("sign")) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
{
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
sb.append("key=").append(key);
if ("MD5".equals(signType)) {
return MD5(sb.toString()).toUpperCase();
} else if ("HMACSHA256".equals(signType)) {
return HMACSHA256(sb.toString(), key);
} else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br> 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口
*/
public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
reqData.put("appid", appId);
reqData.put("mch_id", mchId);
reqData.put("nonce_str", generateNonceStr());
if ("MD5".equals(this.signType)) {
reqData.put("sign_type", "MD5");
} else if ("HMACSHA256".equals(this.signType)) {
reqData.put("sign_type", "HMACSHA256");
}
reqData.put("sign", generateSignature(reqData, apiSecret, signType));
return reqData;
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
}
return data;
}
/**
* 不需要证书的请求
*
* @param url String
* @param reqData 向wxpay post的请求数据
* @param connectTimeoutMs 超时时间,单位是毫秒
* @param readTimeoutMs 超时时间,单位是毫秒
* @return API返回数据
*/
public String requestWithoutCert(String url, Map<String, String> reqData,
int connectTimeoutMs, int readTimeoutMs) throws Exception {
String UTF8 = "UTF-8";
String reqBody = mapToXml(reqData);
URL httpUrl = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setConnectTimeout(connectTimeoutMs);
httpURLConnection.setReadTimeout(readTimeoutMs);
httpURLConnection.connect();
OutputStream outputStream = httpURLConnection.getOutputStream();
outputStream.write(reqBody.getBytes(UTF8));
//获取内容
InputStream inputStream = httpURLConnection.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF8));
final StringBuffer stringBuffer = new StringBuffer();
String line = null;
while ((line = bufferedReader.readLine()) != null) {
stringBuffer.append(line);
}
String resp = stringBuffer.toString();
if (stringBuffer != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resp;
}
/**
* 需要证书的请求
*
* @param url String
* @param reqData 向wxpay post的请求数据 Map
* @param connectTimeoutMs 超时时间,单位是毫秒
* @param readTimeoutMs 超时时间,单位是毫秒
* @return API返回数据
*/
public String requestWithCert(String url, Map<String, String> reqData,
int connectTimeoutMs, int readTimeoutMs) throws Exception {
String UTF8 = "UTF-8";
String reqBody = mapToXml(reqData);
URL httpUrl = new URL(url);
char[] password = mchId.toCharArray();
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certStream, password);
// 实例化密钥库 & 初始化密钥工厂
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
// 创建SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setConnectTimeout(connectTimeoutMs);
httpURLConnection.setReadTimeout(readTimeoutMs);
httpURLConnection.connect();
OutputStream outputStream = httpURLConnection.getOutputStream();
outputStream.write(reqBody.getBytes(UTF8));
//获取内容
InputStream inputStream = httpURLConnection.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, UTF8));
final StringBuffer stringBuffer = new StringBuffer();
String line = null;
while ((line = bufferedReader.readLine()) != null) {
stringBuffer.append(line);
}
String resp = stringBuffer.toString();
if (stringBuffer != null) {
try {
bufferedReader.close();
} catch (IOException e) {
// e.printStackTrace();
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
// e.printStackTrace();
}
}
if (certStream != null) {
try {
certStream.close();
} catch (IOException e) {
// e.printStackTrace();
}
}
return resp;
}
/**
* 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。
*
* @param xmlStr API返回的XML格式数据
* @return Map类型数据
*/
public Map<String, String> processResponseXml(String xmlStr) throws Exception {
String RETURN_CODE = "return_code";
String return_code;
Map<String, String> respData = xmlToMap(xmlStr);
if (respData.containsKey(RETURN_CODE)) {
return_code = respData.get(RETURN_CODE);
} else {
throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
}
if (return_code.equals("FAIL")) {
return respData;
} else if (return_code.equals("SUCCESS")) {
if (this.isSignatureValid(respData, apiSecret, "MD5")) {
return respData;
} else {
throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
}
} else {
throw new Exception(
String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
}
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
*/
public static boolean isSignatureValid(final Map<String, String> data, String key,
String signType)
throws Exception {
String sign = data.get("sign");
return generateSignature(data, key, signType).equals(sign);
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
java.security.MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
*
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key : data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
} catch (Exception ex) {
}
return output;
}
}
class PayInfo {
private String openId;
/**
* 系统内部订单号
*/
private String oid;
/**
* 总费用
*/
private int totalFee;
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public int getTotalFee() {
return totalFee;
}
public void setTotalFee(int totalFee) {
this.totalFee = totalFee;
}
}
class WxRefundRequest {
/**
* 微信订单号
*/
private String transactionId;
/**
* 系统内部退款单号
*/
private String payRefundId;
private Integer totalFee;
private Integer refundFee;
private String refundDesc;
public String getTransactionId() {
return transactionId;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
public String getPayRefundId() {
return payRefundId;
}
public void setPayRefundId(String payRefundId) {
this.payRefundId = payRefundId;
}
public Integer getTotalFee() {
return totalFee;
}
public void setTotalFee(Integer totalFee) {
this.totalFee = totalFee;
}
public Integer getRefundFee() {
return refundFee;
}
public void setRefundFee(Integer refundFee) {
this.refundFee = refundFee;
}
public String getRefundDesc() {
return refundDesc;
}
public void setRefundDesc(String refundDesc) {
this.refundDesc = refundDesc;
}
}