springBoot 微信支付 PC网站微信扫码支付-Native支付
最近项目需求集成支付功能,支付宝支付就不用多说了,官方文档Demo都很详细,仔细搞一下就可以。今天这篇文章主要讲集成PC网站集成Native支付,中间遇到了几个坑,这里给大家讲解下解决方法,相信一定会帮到你。
下面的支付Demo是老版本使用xml传输的格式,新版本使用的是json
api文档: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1
一、采坑大合集
1.当前商户号暂不支持关联该类型的appid
微信支付需要的三个重要参数 :1.mch_id 2.mch_api_key 3.app_id
- mch_id:商户id,这个去微信商户平台注册认证即可
- mch_api_key:注册成功后,进入商户平台->账户中心->API安全->设置API密钥
- app_id:appId,才是接下来的重点,当你根据官方文档说明,去微信公众平台注册账号,并交完认证费300元后,获得了appId,然后再进行appId与mch_id绑定时,提示"当前商户号暂不支持关联该类型的appid"。去社区客服回答:“开放平台网站应用和第三方应用均不支持自助绑定,请悉知”
解决办法: 在微信公众平台注册时,有四种选择:订阅号,服务号,小程序,企业微信。默认创建的应该是订阅号,但是Native支付只能绑定公众平台类型为服务号的appId,时间:2021/09/03,以后是否支持不一定。所以小伙伴们麻溜的去申请一个服务号类型的appId就可以了
2.签名错误,请检查后再试
- 签名错误问题,首先需要检查的是对照官方的api,看看哪些字段是必须的,是否有漏掉
- 字段无误后,发现还是错误,然后网上搜了一圈,说是什么必须按照某个顺序罗列字段才行,然后你就试了各种顺序还是错误。其实并不是你的字段错误,而真的是签名错误。
- 签名校验原理:将所有参数用api_key进行md5加密后生成sign,然后将sign随参数一起发送微信,微信端根据参数也用api_key进行md5加密后生成的sign与你参数的sign进行比较,相同的话就认证成功,不同就认证失败
看上面两段代码有何不同,在生成sign的时候一定要将全部参数都加进去,不能在生成了sign后,还有别的参数添加,这样就会导致签名错误。所以只要保证sign在最后一个就好了
二、springboot集成微信支付Demo(老版本XML)
1.官方SDK下载:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
好多工具类从sdk中copy即可,下面是我的集成demo,
统一下单以及回调接口
WXpay.java
private static String wxpayString="<form name=\"punchout_form\" method=\"post\" action=\"%s\">\n" +
"<input type=\"hidden\" name=\"wxpayUrl\" value=\"%s\">\n" +
"<input type=\"hidden\" name=\"id\" value=\"%s\">\n" +
"<input type=\"submit\" style=\"display:none\" >\n" +
"</form>\n" +
"<script>document.forms[0].submit();</script>\n";
private static String qrCodeString="/wxpay/qrCode";
/**
* 腾讯支付
* @param product
* @param order must 1.order_code 2.money 3.spbill_create_ip
* @return
* @throws Exception
*/
public static String goWXpay(BillingPackageVo product, BillingPackageOrderVo order) {
String result="";
try {
//1.配置请求参数
HashMap<String,String> reqData=new HashMap<>();
reqData.put("appid", app_id);
reqData.put("mch_id", mch_id);
reqData.put("device_info", WEB);
reqData.put("nonce_str", WXpayUtil.generateNonceStr());
reqData.put("sign_type", SignType.MD5+"");
//商品描述
reqData.put("body", product.getBillingPackageName()+",服务期限"+
product.getBillingPeriod()+"天,支持设备"+product.getMaxAccessClientNumber()+"个");
//商品详情
// reqData.put("detail", "微信支付测试");
//订单号
reqData.put("out_trade_no",order.getBillingPackageOrderCode());
//订单总金额,单位为分
reqData.put("total_fee",(long)(order.getOrderAmount()*100)+"");
//终端IP,客户端IP
reqData.put("spbill_create_ip",order.getSpbillCreateIp());
//通知地址
reqData.put("notify_url",notify_url);
//交易类型
reqData.put("trade_type", TradeType.NATIVE+"");
//商品Id,trade_type=NATIVE时,此参数必传。
reqData.put("product_id",order.getBillingPackageOrderCode());
// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
reqData.put("attach",String.valueOf(null==order.getAccessClientId()?0:order.getAccessClientId()));
reqData.put("sign", WXpayUtil.generateSignature(reqData, api_key, SignType.MD5));
// 2.map转换成xml
String reqBody = WXpayUtil.mapToXml(reqData);
log.info("订单号:"+order.getBillingPackageOrderCode()+",微信支付请求xml:"+reqBody);
//3.发送支付请求
String resXml= HttpClient.sendPostDataByXml(gatewayUrl, reqBody);
log.info("订单号:"+order.getBillingPackageOrderCode()+",微信支付返回xml:"+resXml);
//4.xml转换成map
Map<String, String> resMap = WXpayUtil.xmlToMap(resXml);
String codeUrl=(SUCCESS.equals(resMap.get("return_code")) && SUCCESS.equals(resMap.get("result_code")))?resMap.get("code_url"):"";
//5.组装返回值
result=String.format(wxpayString,qrCodeString,codeUrl,order.getId());
log.info("微信支付跳转信息:"+result);
return result;
}catch (Exception e){
e.printStackTrace();
log.info("微信支付请求失败,订单号:"+order.getBillingPackageOrderCode());
return result;
}
}
/**
* 微信支付回调
* @param request
* @return
*/
@ResponseBody
@RequestMapping("/wechatNotify")
public String weChatNotify(HttpServletRequest request) throws Exception {
log.info("微信支付成功, 进入异步通知接口...");
Map<String,String> returnMap=new HashMap<>();
returnMap.put("return_code",SUCCESS);
returnMap.put("return_msg","");
Map<String, String> notifyMap = WXpayUtil.getNotifyParameter(request); // 转换成map
if (!this.isPayResultNotifySignatureValid(notifyMap)) {
// 签名错误,如果数据里没有sign字段,也认为是签名错误
log.info("签名错误!!!!");
returnMap.put("return_msg","sign not valid");
return WXpayUtil.mapToXml(returnMap);
}
//订单号
String orderCode = notifyMap.get("out_trade_no");
String resultCode = notifyMap.get("result_code");
//格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。
Long paymentTime =DataUtil.stringIntegerSpecialDate(notifyMap.get("time_end"));
String paymentCode= notifyMap.get("transaction_id");
// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
String attach= notifyMap.get("attach");
//这里做其他业务操作
returnMap.put("return_msg","");
return WXpayUtil.mapToXml(returnMap);
}
/**
* 判断支付结果通知中的sign是否有效
*
* @param reqData 向wxpay post的请求数据
* @return 签名是否有效
* @throws Exception
*/
private boolean isPayResultNotifySignatureValid(Map<String, String> reqData) throws Exception {
String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE);
SignType signType;
if (signTypeInData == null) {
signType = SignType.MD5;
} else {
signTypeInData = signTypeInData.trim();
if (signTypeInData.length() == 0) {
signType = SignType.MD5;
}
else if (WXPayConstants.MD5.equals(signTypeInData)) {
signType = SignType.MD5;
}
else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) {
signType = SignType.HMACSHA256;
}
else {
throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));
}
}
return this.isSignatureValid(reqData,api_key, signType);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
return false;
}
String sign = data.get(WXPayConstants.FIELD_SIGN);
return WXpayUtil.generateSignature(data, key, signType).equals(sign);
}
WXpayUtil.java
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = WXpayUtil.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) {
ex.printStackTrace();
}
return data;
} catch (Exception ex) {
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 = WXpayUtil.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) {
ex.printStackTrace();
}
return output;
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key, SignType 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(WXPayConstants.FIELD_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 (SignType.MD5.equals(signType)) {
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 生成 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 加密结果
* @throws Exception
*/
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();
}
/**
* 从request的inputStream中获取参数
* @param request
* @return
* @throws Exception
*/
public static Map<String, String> getNotifyParameter(HttpServletRequest request) throws Exception {
InputStream inputStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, length);
}
outSteam.close();
inputStream.close();
// 获取微信调用我们notify_url的返回信息
String resultXml = new String(outSteam.toByteArray(), "utf-8");
log.info("微信异步通知返回xml:"+resultXml);
Map<String, String> notifyMap = xmlToMap(resultXml);
log.info("********************** 微信支付返回参数**********************");
notifyMap.forEach((key, value) -> {
log.info((key+":"+value));
});
log.info("********************************************");
return notifyMap;
}
/**
* 组装xml
* @return
* @throws ParserConfigurationException
*/
public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
return documentBuilderFactory.newDocumentBuilder();
}
public static Document newDocument() throws ParserConfigurationException {
return newDocumentBuilder().newDocument();
}
WXPayConfig.java
@Configuration
public class WXPayConfig implements InitializingBean {
// 公众账号ID
public static String app_id;
// 商户号
public static String mch_id;
// 商户的key【API密匙】
public static String api_key;
// 微信网关
public static String gatewayUrl;
// 服务器异步通知页面路径
public static String notify_url;
@Value("${wxpay.app_id}")
private String getApp_id;
@Value("${wxpay.mch_id}")
private String getMch_id;
@Value("${wxpay.api_key}")
private String getApi_key;
@Value("${wxpay.gatewayUrl}")
private String getGatewayUrl;
@Value("${project_address}")
private String getProject_address;
private String getNotify_url="/wxpay/wechatNotify";
@Override
public void afterPropertiesSet() throws Exception {
WXPayConfig.app_id=this.getApp_id;
WXPayConfig.mch_id=this.getMch_id;
WXPayConfig.api_key=this.getApi_key;
WXPayConfig.gatewayUrl=this.getGatewayUrl;
WXPayConfig.notify_url=this.getProject_address+this.getNotify_url;
}
}
httpClient.java
/**
* post请求传输xml数据
* @param url
* @param xml
* @return
* @throws ClientProtocolException
* @throws IOException
*/
public static String sendPostDataByXml(String url, String xml){
log.info("url:"+url+",body:"+xml);
String result = "";
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建post方式请求对象
HttpPost httpPost = new HttpPost(url);
// 设置参数到请求对象中
StringEntity stringEntity = new StringEntity(xml, "UTF-8");
httpPost.addHeader("Content-Type", "text/xml");
httpPost.setEntity(stringEntity);
// 执行请求操作,并拿到结果(同步阻塞)
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpPost);
// 获取结果实体
// 判断网络连接状态码是否正常(0--200都数正常)
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
result = EntityUtils.toString(response.getEntity(), "UTF-8");
log.info("xml请求返回结果:"+result);
}
} catch (Exception e) {
e.printStackTrace();
log.info("请求失败:"+url);
}finally {
// 释放链接
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}