- 在Java中,集成微信支付,十分的简单,主要分为如下几步
- 一、导入依赖
- 二、编写微信支付的配置
- 三、编写下单
- 3.1 编写支付工具类 WxPayUtil
- 3.2 编写Controller,调用封装的工具类方法
- 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了">如果,二次签名返回的数据中,都是不为null的,说明,有很大可能,是签名成功的,或者,可以通过 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了
- 四、APP进行支付测试
本文没有与申请微信支付相关的内容(申请微信支付过于复杂…..) 本文前提: 已经申请好了对应的微信支付商户号,并且已经签约了对应的支付产品
在Java中,集成微信支付,十分的简单,主要分为如下几步
1. 导入依赖
2. 编写微信支付的配置
3. 编写下单,进行测试
一、导入依赖
<dependency><groupId>com.github.wxpay</groupId><artifactId>wxpay-sdk</artifactId><version>0.0.3</version></dependency>
二、编写微信支付的配置
这里,通过一个配置类,读取application.yml 中的配置,再通过 @Configuration+@Bean的方式,注入到容器中。
public class WxPayScanConfig implements WXPayConfig {private PayInfoConfig payInfoConfig;private byte[] certData;/*** 使用类加载的方式,取出证书,否则,当发布打成Jar包的时候,会读取不到Jar包中的东西*/public WxPayScanConfig(PayInfoConfig payInfoConfig) {this.payInfoConfig = payInfoConfig;try {InputStream certStream = this.getClass().getResourceAsStream("/apiclient_cert.p12");this.certData = new byte[certStream.available()];certStream.read(this.certData);certStream.close();} catch (IOException e) {e.printStackTrace();}}@Overridepublic String getAppID() {return payInfoConfig.getWxpay().getAppId();}@Overridepublic String getMchID() {return payInfoConfig.getWxpay().getMchId();}@Overridepublic String getKey() {return payInfoConfig.getWxpay().getMchKey();}@Overridepublic InputStream getCertStream() {return new ByteArrayInputStream(this.certData);}@Overridepublic int getHttpConnectTimeoutMs() {return 8000;}@Overridepublic int getHttpReadTimeoutMs() {return 100000;}}
三、编写下单
3.1 编写支付工具类 WxPayUtil
@Slf4j@Componentpublic class WxPayUtil {@Autowiredprivate WXPay wxPay;@Autowiredprivate PayInfoConfig payInfoConfig;@Autowiredprivate PayCommonUtil payCommonUtil;public Map<String,String> appNotify(String body,String price){SortedMap<String, String> data = new TreeMap<>();String outTradeNo = getRandomString(32);data.put("body", body);data.put("out_trade_no", outTradeNo);data.put("total_fee", price);data.put("spbill_create_ip", "58.23.48.202");data.put("notify_url", "http://localhost:8080/");data.put("trade_type", "APP");Map<String, String> resp = null;try {resp = wxPay.unifiedOrder(data);resp.put("out_trade_no",outTradeNo);} catch (Exception e) {log.info("微信支付出现错误!" +e.getMessage());e.printStackTrace();}return resp;}public Map<String,String> refund(String outTradeNo){SortedMap<String,String> data = new TreeMap<>();data.put("appid", payInfoConfig.getWxpay().getAppId());data.put("mch_id", payInfoConfig.getWxpay().getMchId());data.put("nonce_str", getRandomString(32));data.put("out_trade_no",outTradeNo);data.put("out_refund_no", getRandomString(32));Map<String, String> orderQuery = queryOrder(outTradeNo);data.put("total_fee",orderQuery.get("total_fee"));data.put("refund_fee",orderQuery.get("total_fee"));data.put("sign", payCommonUtil.createSign("UTF-8",data));try {Map<String, String> resp = wxPay.refund(data);System.out.println(resp);return resp;} catch (Exception e) {e.printStackTrace();}return null;}public Map<String,String> queryRefund(String outTradeNo){Map<String, String> data = new HashMap<>(0);data.put("out_trade_no", outTradeNo);try {return wxPay.refundQuery(data);} catch (Exception e) {e.printStackTrace();}return null;}/*** 获取app二次签名数据* @param resp 微信统一下单之后返回的data* @return Map<String,String>*/public Map<String,String> appPaySignDouble(Map<String,String> resp){log.info("response=" + resp);SortedMap<String, String> signParam = new TreeMap<>();//app_idString prepayid = resp.get("prepay_id");signParam.put("appid", resp.get("appid"));signParam.put("partnerid", resp.get("mch_id"));signParam.put("prepayid", prepayid);signParam.put("package", "Sign=WXPay");signParam.put("noncestr", resp.get("nonce_str"));//北京时间时间戳signParam.put("timestamp", System.currentTimeMillis()/1000 + "");signParam.put("paySign", payCommonUtil.createSign("UTF-8", signParam));signParam.put("outTradeNo",resp.get("out_trade_no"));return signParam;}public Map<String,String> queryOrder(String outTradeNo){Map<String, String> data = new HashMap<>(0);data.put("out_trade_no", outTradeNo);try {return wxPay.orderQuery(data);} catch (Exception e) {e.printStackTrace();}return null;}public String getRandomString(int length){//定义一个字符串(A-Z,a-z,0-9)即62位;String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";//由Random生成随机数Random random=new Random();StringBuffer sb=new StringBuffer();//长度为几就循环几次for(int i=0; i<length; ++i){//产生0-61的数字int number=random.nextInt(62);//将产生的数字通过length次承载到sb中sb.append(str.charAt(number));}//将承载的字符转换成字符串return sb.toString();}}
其中用到的工具类:
PayCommonUtil 主要作用为生成签名以及解析xml
@Componentpublic class PayCommonUtil {@Autowiredprivate PayInfoConfig payInfoConfig;/*** 定义签名,微信根据参数字段的ASCII码值进行排序 加密签名,故使用SortMap进行参数排序* @param characterEncoding 字符编码* @param parameters 需要签名的参数* @return sign string。class*/public String createSign(String characterEncoding,SortedMap<String,String> parameters){StringBuilder sb = new StringBuilder();Set<Map.Entry<String, String>> es = parameters.entrySet();for (Map.Entry<String, String> entry : es) {String k = entry.getKey();Object v = entry.getValue();if (null != v && !"".equals(v)&& !"sign".equals(k) && !"key".equals(k)) {sb.append(k).append("=").append(v).append("&");}}//最后加密时添加商户密钥,由于key值放在最后,所以不用添加到SortMap里面去,单独处理,编码方式采用UTF-8sb.append("key=").append(payInfoConfig.getWxpay().getMchKey());return MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();}/*** 将封装好的参数转换成Xml格式类型的字符串* @param parameters 封装好的参数* @return String xml类型的字符串*/public String getRequestXml(SortedMap<String,String> parameters){StringBuilder sb = new StringBuilder();sb.append("<xml>");Set<Map.Entry<String, String>> es = parameters.entrySet();for (Map.Entry<String, String> entry : es) {String k = entry.getKey();String v = entry.getValue();if ("sign".equalsIgnoreCase(k)) {} else if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k)) {sb.append("<").append(k).append(">").append("<![CDATA[").append(v).append("]]></").append(k).append(">");} else {sb.append("<").append(k).append(">").append(v).append("</").append(k).append(">");}}sb.append("<" + "sign" + ">" + "<![CDATA[").append(parameters.get("sign")).append("]]></").append("sign").append(">");sb.append("</xml>");return sb.toString();}/*** 验证回调签名* @return Boolean, 是否正确*/public boolean isTenpaySign(Map<String, String> map) {String characterEncoding="utf-8";String charset = "utf-8";String signFromAPIResponse = map.get("sign");if (signFromAPIResponse == null || "".equals(signFromAPIResponse)) {System.out.println("API返回的数据签名数据不存在,有可能被第三方篡改!!!");return false;}System.out.println("服务器回包里面的签名是:" + signFromAPIResponse);//过滤空 设置 TreeMapSortedMap<String,String> packageParams = new TreeMap<>();for (String parameter : map.keySet()) {String parameterValue = map.get(parameter);String v = "";if (null != parameterValue) {v = parameterValue.trim();}packageParams.put(parameter, v);}StringBuilder sb = new StringBuilder();Set<Map.Entry<String, String>> es = packageParams.entrySet();for (Object e1 : es) {@SuppressWarnings("unchecked")Map.Entry<String, String> entry = (Map.Entry<String, String>) e1;String k = entry.getKey();String v = entry.getValue();if (!"sign".equals(k) && null != v && !"".equals(v)) {sb.append(k).append("=").append(v).append("&");}}sb.append("key=").append(payInfoConfig.getWxpay().getMchKey());//将API返回的数据根据用签名算法进行计算新的签名,用来跟API返回的签名进行比较//算出签名String resultSign = "";String tobesign = sb.toString();if (null == charset || "".equals(charset)) {resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();}else{try{resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();}catch (Exception e) {resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();}}String tenpaySign = (packageParams.get("sign")).toUpperCase();return tenpaySign.equals(resultSign);}public String httpsRequest(String requestUrl, String requestMethod, String outputStr){try{// 创建SSLContext对象,并使用我们指定的信任管理器初始化TrustManager[] tm ={ null };SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");sslContext.init(null, tm, new java.security.SecureRandom());// 从上述SSLContext对象中得到SSLSocketFactory对象SSLSocketFactory ssf = sslContext.getSocketFactory();URL url = new URL(requestUrl);HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();conn.setDoOutput(true);conn.setDoInput(true);conn.setUseCaches(false);// 设置请求方式(GET/POST)conn.setRequestMethod(requestMethod);conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");// 当outputStr不为null时向输出流写数据if (null != outputStr){OutputStream outputStream = conn.getOutputStream();// 注意编码格式outputStream.write(outputStr.getBytes(StandardCharsets.UTF_8));outputStream.close();}// 从输入流读取返回内容InputStream inputStream = conn.getInputStream();InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);BufferedReader bufferedReader = new BufferedReader(inputStreamReader);String str = null;StringBuilder buffer = new StringBuilder();while ((str = bufferedReader.readLine()) != null){buffer.append(str);}// 释放资源bufferedReader.close();inputStreamReader.close();inputStream.close();inputStream = null;conn.disconnect();return buffer.toString();} catch (ConnectException ce) {// log.error("连接超时:{}", ce);} catch (Exception e) {}return null;}}
MD5Util 主要作用,用于数据的加密,默认方式为MD5加密
public class MD5Util {private static String byteArrayToHexString(byte b[]) {StringBuffer resultSb = new StringBuffer();for (int i = 0; i < b.length; i++)resultSb.append(byteToHexString(b[i]));return resultSb.toString();}private static String byteToHexString(byte b) {int n = b;if (n < 0)n += 256;int d1 = n / 16;int d2 = n % 16;return hexDigits[d1] + hexDigits[d2];}public static String MD5Encode(String origin, String charsetname) {String resultString = null;try {resultString = new String(origin);MessageDigest md = MessageDigest.getInstance("MD5");if (charsetname == null || "".equals(charsetname))resultString = byteArrayToHexString(md.digest(resultString.getBytes()));elseresultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));} catch (Exception exception) {}return resultString;}private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5","6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };}
3.2 编写Controller,调用封装的工具类方法
这里需要注意的是,需要进行两步操作,分别为
- 统一下单(appNotify)
- 二次签名(将统一下单中的一部分数据,进行二次的签名,详见 微信支付开发文档 )
需要注意的是,wxpay和alipay不同的是,wxpay的price,是按照分来计算的(即1=1分,输入为100,代表收取1块钱),且不能包含小数点,所以在传入值的时候,需要将Double强转成int
@Autowiredprivate WxPayUtil wxPayUtil;@GetMapping("/test")public ResponseModel test(String title, Double price) {Map<String, String> response = wxPayUtil.appNotify(title, ((int)(price * 100)) + "");Map<String, String> result = wxPayUtil.appPaySignDouble(response);return ResponseModel.success("签名成功!", result);}
二次签名之后返回的数据如下所示
{"appid": "","noncestr": "s3rziO04IYbvhcvU","outTradeNo": "a14As0d1zqmnVWvN1FMriQmPNu3lPW5P","package": "Sign=WXPay","partnerid": "","paySign": "F549A5ADE9E767C49A8432AA2F9297E8","prepayid": "wx09165100326315c5XXXXXXXXXXXXXXXXXX","timestamp": "1599641460"},
如果,二次签名返回的数据中,都是不为null的,说明,有很大可能,是签名成功的,或者,可以通过 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了
如下,验证应该的sign值为 F549A5ADE9E767C49A8432AA2F9297E8,实际返回给前端的paySign也是 F549A5ADE9E767C49A8432AA2F9297E8,即代表签名成功,可以考虑发起支付了
四、APP进行支付测试
APP 端的测试,详见另一篇文章
