微信小程序支付
基础信息
appId
appSecret
mchID
key
notifyUrl
certPath = classpath:cert/apiclient_cert.p12
证书说明
欢迎使用微信支付!
附件中的三份文件(证书pkcs12格式、证书pem格式、证书密钥pem格式),为接口中强制要求时需携带的证书文件。
证书属于敏感信息,请妥善保管不要泄露和被他人复制。
不同开发语言下的证书格式不同,以下为说明指引:
证书pkcs12格式(apiclient_cert.p12)
包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份
部分安全性要求较高的API需要使用该证书来确认您的调用身份
windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号(如:1900006031)
证书pem格式(apiclient_cert.pem)
从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制
部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供
您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem
证书密钥pem格式(apiclient_key.pem)
从apiclient_cert.p12中导出密钥部分的文件,为pem格式
部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供
您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem
备注说明:
由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书, 2018年3月6日后, 不再提供CA证书文件(rootca.pem)下载
统一下单
@Override public Map<String, String> payHandle( HttpServletRequest request, HttpServletResponse response, OrderSale orderSale, String paymentDescription, String openid ) { Map<String, String> paramMap = new TreeMap<>(); paramMap.put("appid", appId); paramMap.put("mch_id", mchID); //paramMap.put("device_info",); paramMap.put("nonce_str", DigestUtils.md5Hex(UUID.randomUUID() + RandomStringUtils.randomAlphabetic(30))); //paramMap.put("sign_type",); paramMap.put("body", StringUtils.abbreviate(paymentDescription.replaceAll("[^0-9a-zA-Z\\u4e00-\\u9fa5 ]", ""), 600)); //paramMap.put("detail", ); //paramMap.put("attach", ); paramMap.put("out_trade_no", orderSale.getSn()); //paramMap.put("fee_type", ); paramMap.put("total_fee", String.valueOf(orderSale.getAmount().multiply(new BigDecimal(100)).setScale(0, RoundingMode.DOWN))); // paramMap.put("spbill_create_ip", request.getHeader("X-Real-IP")); paramMap.put("spbill_create_ip", HttpUtils.getIp()); //paramMap.put("time_start", ); //paramMap.put("time_expire", ); //paramMap.put("goods_tag", ); paramMap.put("notify_url", notifyUrl); paramMap.put("trade_type", "JSAPI"); //paramMap.put("product_id", ); //paramMap.put("limit_pay", ); paramMap.put("openid", openid); //paramMap.put("receipt", ); //paramMap.put("scene_info", ); paramMap.put("sign", HttpUtils.generateSign(paramMap, key)); logger.info("payHandle request:" + paramMap.toString()); String result = null; try { result = HttpUtils.doPost(UNIFIED_ORDER, HttpUtils.mapToXml(paramMap)); } catch (Exception e) { e.printStackTrace(); } logger.info("payHandle response:" + result); Map<String, String> resultMap = new HashMap<>(); try { resultMap = HttpUtils.xmlToMap(result); } catch (Exception e) { e.printStackTrace(); logger.error(e.getMessage(), e); } Map<String, String> resMap = new HashMap<>(); if (StringUtils.equals(resultMap.get("return_code"), "SUCCESS")) { resMap.put("appId", resultMap.get("appid")); resMap.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); resMap.put("nonceStr", DigestUtils.md5Hex(UUID.randomUUID() + RandomStringUtils.randomAlphabetic(30))); resMap.put("signType", "MD5"); resMap.put("package", "prepay_id=" + resultMap.get("prepay_id")); //二次签名 resMap.put("paySign", HttpUtils.generateSign(resMap, key)); resMap.put("return_code", resultMap.get("return_code")); resMap.put("url", notifyUrl); resMap.put("orderNumber", orderSale.getSn()); resMap.put("orderId", orderSale.getId().toString()); } else { resMap.put("status", "failure"); resMap.put("message", resultMap.get("return_msg")); } logger.info("二次签名:" + resMap.toString()); return resMap; }
支付回调
/** * 支付回调接口,微信支付成功后会自动调用 * * @param request * @param response * @throws Exception */ @RequestMapping(value = "/pay/notify", method = RequestMethod.POST) public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception { logger.info("=====>支付回调接口 /store/notify start ..."); BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream())); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line); } br.close(); Map<String, String> map = HttpUtils.xmlToMap(sb.toString()); logger.info("/store/notify:" + map.toString()); String resXml = ""; if ("SUCCESS".equals(map.get("return_code"))) { //根据微信官网的介绍,此处不仅对回调的参数进行验签,还需要对返回的金额与系统订单的金额进行比对等 if (HttpUtils.validateSign(map, payService.getKey())) { orderSaleService.afterPayment(map.get("out_trade_no")); //通知微信服务器已经支付成功 resXml = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } } else { resXml = "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[报文为空]]></return_msg></xml> "; } BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); out.write(resXml.getBytes()); out.flush(); out.close(); logger.info(resXml); logger.info("=====>支付回调接口 /store/notify end ..."); }
HttpUtils
import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.Consts; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.servlet.http.HttpServletRequest; import javax.xml.parsers.DocumentBuilder; 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 java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.*; public class HttpUtils { private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class); private static String[] IEBrowserSignals = {"MSIE", "Trident", "Edge"}; public static boolean isMSBrowser(HttpServletRequest request) { String userAgent = request.getHeader("User-Agent"); for (String signal : IEBrowserSignals) { if (userAgent.contains(signal)) return true; } return false; } public static String doPost(String url, Map<String, String> mapParams) { try (CloseableHttpClient httpclient = HttpClients.createDefault()) { HttpPost httpPost = new HttpPost(url); //封装请求参数 if (mapParams != null) { List<BasicNameValuePair> list = new ArrayList<>(); for (Map.Entry<String, String> entry : mapParams.entrySet()) { list.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(list, "utf-8"); formEntity.setContentType("Content-Type:application/json"); httpPost.setEntity(formEntity); } try (CloseableHttpResponse response = httpclient.execute(httpPost)) { return EntityUtils.toString(response.getEntity()); } } catch (IOException e) { e.printStackTrace(); } return null; } public static String doPost(String url, String xml) { try (CloseableHttpClient httpclient = HttpClients.createDefault()) { HttpPost httpPost = new HttpPost(url); httpPost.setEntity(new StringEntity(xml, "UTF-8")); try (CloseableHttpResponse response = httpclient.execute(httpPost)) { return EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (IOException e) { e.printStackTrace(); } return null; } public static String doPost(String url, String jsonParam, String token) { try (CloseableHttpClient httpclient = HttpClients.createDefault()) { HttpPost httpPost = new HttpPost(url); httpPost.setHeader("Content-Type", "application/json;charset=utf8"); if (StringUtils.isNotBlank(token)) httpPost.setHeader("Authorization", token); StringEntity stringEntity = new StringEntity(jsonParam); stringEntity.setContentEncoding("UTF-8"); stringEntity.setContentType("application/json"); httpPost.setEntity(stringEntity); try (CloseableHttpResponse response = httpclient.execute(httpPost)) { return EntityUtils.toString(response.getEntity()); } } catch (IOException e) { e.printStackTrace(); } return null; } public static String doGet(String url, Map<String, String> mapParams) { try (CloseableHttpClient httpclient = HttpClients.createDefault()) { //封装请求参数 if (mapParams != null) { List<BasicNameValuePair> list = new ArrayList<>(); for (Map.Entry<String, String> entry : mapParams.entrySet()) { list.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } url = url + "?" + EntityUtils.toString(new UrlEncodedFormEntity(list, Consts.UTF_8)); } HttpGet httpget = new HttpGet(url); try (CloseableHttpResponse response = httpclient.execute(httpget)) { return EntityUtils.toString(response.getEntity()); } } catch (IOException e) { e.printStackTrace(); } return null; } /** * XML格式字符串转换为Map * * @param strXML * @return * @throws Exception */ public static Map<String, String> xmlToMap(String strXML) throws Exception { try { Map<String, String> data = new HashMap<>(); DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder(); InputStream stream = new ByteArrayInputStream(strXML.getBytes(StandardCharsets.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()); } } stream.close(); return data; } catch (Exception ex) { WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML); throw ex; } } /** * 将Map转换为XML格式的字符串 * * @param data Map类型数据 * @return XML格式的字符串 * @throws Exception */ public static String mapToXml(Map<String, String> data) throws Exception { org.w3c.dom.Document document = WXPayXmlUtil.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", ""); writer.close(); return output; } /** * 生成签名 * * @param parameterMap * @param key * @return */ public static String generateSign(Map<String, ?> parameterMap, String key) { return StringUtils.upperCase(DigestUtils.md5Hex(joinKeyValue(new TreeMap<>(parameterMap), null, "&key=" + key, "&", true))); } /** * 连接Map键值对 * * @param map * @param prefix * @param suffix * @param separator * @param ignoreEmptyValue * @param ignoreKeys * @return */ public static String joinKeyValue( Map<Object, Object> map, String prefix, String suffix, String separator, boolean ignoreEmptyValue, String... ignoreKeys ) { List<String> list = new ArrayList<>(); if (map != null) { for (Map.Entry<Object, Object> entry : map.entrySet()) { String key = entry.getKey().toString(); String value = ConvertUtils.convert(entry.getValue()); if (StringUtils.isNotEmpty(key) && !ArrayUtils.contains(ignoreKeys, key) && (!ignoreEmptyValue || StringUtils.isNotEmpty(value))) { list.add(key + "=" + (value != null ? value : "")); } } } return (prefix != null ? prefix : "") + StringUtils.join(list, separator) + (suffix != null ? suffix : ""); } /** * 获取ip * * @return */ public static String getIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { e.printStackTrace(); } return null; } /** * 通知接口验签 * * @param parameterMap * @param key * @return */ public static boolean validateSign(Map<String, String> parameterMap, String key) { String sign = parameterMap.get("sign"); logger.info("微信支付,传入的sign进行校验" + sign); if (StringUtils.isEmpty(sign)) { logger.info("微信支付,sign参数为空!"); return false; } String md5Hex = getSign(parameterMap, key); if (!md5Hex.equals(sign.toUpperCase())) { logger.info("微信支付,签名错误"); return false; } return true; } public static String getSign(Map<String, String> parameterMap, String key) { // 将Map转换为TreeMap Set<Map.Entry<String, String>> parameterMapSet = parameterMap.entrySet(); Iterator<Map.Entry<String, String>> hashMapIterator = parameterMapSet.iterator(); Map<String, String> treeMap = new TreeMap<String, String>(); while (hashMapIterator.hasNext()) { Map.Entry<String, String> param = hashMapIterator.next(); if (!"sign".equals(param.getKey())) { treeMap.put(param.getKey(), param.getValue()); } } // 拼接字符串 StringBuilder sb = new StringBuilder(); Set<Map.Entry<String, String>> treeMapSet = treeMap.entrySet(); Iterator<Map.Entry<String, String>> treeMapIterator = treeMapSet.iterator(); while (treeMapIterator.hasNext()) { Map.Entry<String, String> param = treeMapIterator.next(); // 校验空值 if (StringUtils.isEmpty(param.getValue())) { if (treeMapIterator.hasNext()) { } else { sb.replace(sb.toString().length() - 1, sb.toString().length(), ""); } continue; } sb.append(param.getKey()); sb.append("="); sb.append(param.getValue()); if (treeMapIterator.hasNext()) { sb.append("&"); } } if (StringUtils.isEmpty(sb.toString())) { throw new RuntimeException("传入的参数为空"); } // 拼接key sb.append("&key=").append(key); return DigestUtils.md5Hex(sb.toString()).toUpperCase(); } public static void main(String[] args) { } }