目录
关于API v3
各参数的解释
商户API证书序列号,申请证书后就有对应的。
商户私钥文件 ——目的为了做签名。如何加载商户私钥
微信服务器地址
接收结果通知地址
接口规则
定时更新平台证书功能
验签器的获取
获取Http对象
APIV3接口
Native支付流程
1、生成订单(当用户确认下单后,在后台系统,生成一个订单,此时还未支付呢。举个栗子:比如你在拼多多下单,是不是点击购买后,会有一个订单,然后显示你在10分钟之内支付。)
2、调用统一下单API 。
3.调用API之后,微信会返回结果也就是上面的response,native支付就会返回一个code_url
4.我们就把那个code_url变成二维码图片展示给用户.
5.签名原理
6.回调(重难点)
解释
通知规则
通知报文
重点来了:1.下面是对回调数据的处理.
2.验证签名
异常处理
3.报文解密
通知应答
微信V3支付——1
关于微信支付需要的基本配置,以及数字签名等,已在第一篇文章讲解,下面是开始代码篇。
关于API V3和V2的区别 (超级重要)
这个是V2的文档注意区分啊 这个是V3的文档
V2是要xml来转换数据的,V3是json来转换。 V2的签名是需要拼接字符串和密钥什么的,V3是不用的。 关于签名这东西,本人确实没有讲清楚。等有空一定把签名详细补上。但是其他是没有问题的。
为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。
相较于之前的微信支付API,主要区别是:
遵循统一的REST的设计风格使用JSON作为数据交互的格式,不再使用XML使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256不再要求携带HTTPS客户端证书(仅需携带证书序列号)使用AES-256-GCM,对回调中的关键信息进行加密保护看不懂没关系,看后面进行了。
各参数的解释
商户API证书序列号,申请证书后就有对应的。
作用:微信会根据序列号找到我们对应的证书,从证书中解密出我们的公钥,对我们的请求进行验签。
商户私钥文件 ——目的为了做签名。如何加载商户私钥
商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem
中。商户开发者可以使用方法PemUtil.loadPrivateKey()
加载证书。
# 示例:私钥存储在文件PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey( new FileInputStream("存放路径"));# 示例:私钥为String字符串PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey( new ByteArrayInputStream(privateKey.getBytes("utf-8")));
为什么我们不直接把私钥写进去呢?还要从证书中获取私钥,不麻烦吗? 我觉得是因为为了安全等着想,私钥不能够直接放到配置文件中吧。反正Ctrl+C ,V把代码复制来用,就行了。
微信服务器地址
wxpay.domain=""
接收结果通知地址
wxpay.notify-domain =""
接口规则
步骤1和2就是请求和发送, 其中就是加签和验签,下面这个图就就" 图X1"吧,方便后面的讲解
定时更新平台证书功能
我们把这里的代码(除了最后一行的代码)当成一个方法,方法名叫wxPayClient(),返回CloseableHttpClient。方便下面讲调用统一下单API 中用。
// 获取证书管理器实例certificatesManager = CertificatesManager.getInstance();// 向证书管理器增加需要自动更新平台证书的商户信息certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));// ... 若有多个商户号,可继续调用putMerchant添加商户信息// 从证书管理器中获取verifierverifier = certificatesManager.getVerifier(mchId);WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) .withValidator(new WechatPay2Validator(verifier))// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新CloseableHttpClient httpClient = builder.build();// 后面跟使用Apache HttpClient一样CloseableHttpResponse response = httpClient.execute(...);
验签器的获取
// 获取证书管理器实例
certificatesManager = CertificatesManager.getInstance();
//私钥签名对象
new PrivateKeySigner(商户序列号, 商户私钥);
//对称加密对象 ,图X1中的APIV3 密钥加密解密
apiV3Key.getBytes(StandardCharsets.UTF_8)
// 从证书管理器中获取verifier验签器
verifier = certificatesManager.getVerifier(mchId);
总结:先获取证书管理器,然后往里面添加信息,然后从管理器中获取验签器.
获取Http对象
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId,商户序列号, 商户私钥)
.withValidator(new WechatPay2Validator(verifier))
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
//这一行代码,就执行了请求和响应,包括验签和加签等等,因为上面已经为这个http做好了配置。就是前面的图X1的步骤一二的过程.
CloseableHttpResponse response = httpClient.execute(...);
APIV3接口
我们拿native支付来说,URL就是我们请求这个支付方法的接口
wxpay.domain就是服务器地址。比如百度,就是 https://baidu.com。
一般URL我们都在代码中分成两部分,比如native支付就分为
https://api.mch.weixin.qq.com(A)+ /v3/pay/transactions/native(B)
A就是domain上写的那个。 然后下单接口为B ,我们可以枚举好B,比如app支付,native支付
小程序支付,等等。 然后用A+B就可以方便的填写URL了。
Native支付流程
把这图叫图X2,方便后面用它。
我们需要做的,只是商户后台系统需要做的事,你自己看图片就可以看出来了,下面我一一解释。
1、生成订单(当用户确认下单后,在后台系统,生成一个订单,此时还未支付呢。举个栗子:比如你在拼多多下单,是不是点击购买后,会有一个订单,然后显示你在10分钟之内支付。)
比如在数据库中有一个order表,然后有order类,用户一下单,后台就会自动生成订单,然后存入数据库中。一般先通过产品表,获取产品信息,然后生成订单表。
可以在生成订单前,查询数据库有没有改用户已经存在的未支付的该订单,有就不在新生成订单了。
2、调用统一下单API 。
因为官方说V3使用JSON作为数据交互的格式,不再使用XML。所以我们要把参数通过json形式传输。下面是调用统一下单API的流程
//请求微信支付接口
上面的定时更新平台证书功能第一行我解释了。我们现在注入方法
@Resourceprivate CloseableHttpClient wxPayClient;
HttpPost httpPost = new HttpPost("domain+支付类型");//在上面的APIV3接口我解释过了//设置body参数,以json格式Gson gson =new Gson(); //JSONObject也行,Jackson也行Map params= new HashMap();params.put();params.put();.........String json = gson.toJson(params);/*StringEntity可以自己指定ContentType,而默认值是 text/plain,数据的形式就非常自由了,可以组织成自己想要的任何形式,一般用来存储json数据*/StringEntity entity =new StringEntity(json,"utf-8");//要求请求体内容为json格式entity.setContentType(application/json);//设置请求体 httpPost.setEntity(entity);//接受的响应内容也要为json格式httpPost.addHeader("Accept", "application/json"); CloseableHttpResponse response = wxPayClient.execute(httpPost);
因为我们前面已经对httpClient 进行了自动验签的配置,这里我们只需要调用wxPayClient(方法)拿到上面那个CloseableHttpClient(也就是我们的wxPayClient)就好了,然后把请求+参数放进去即可。(CloseableHttpResponse response = wxPayClient.execute(httpPost);所以长这样)
参数的名字类型等,这里有,自己参考。
3.调用API之后,微信会返回结果也就是上面的response,native支付就会返回一个code_url
那么如何去获取那个code_url呢。返回结果是一个json类型。
虽然返回数据只有code_url,但是因为response携带有很多其他东西(httpResponse.getEntity()
是返回这次回复(即 request
的响应)的实体信息,也就是整个 HTTP
响应内容,包括头部、数据区。)。而我们只要code_url。
EntityUtils.toString(httpResponse.getEntity(),"UTF-8");
就是帮你把 ResponseEntity
这个实例的各项有用的值(以 UTF-8
的编码)打印出来看,一个帮助方法而已。
String bodyString=EntityUtils.toString(response.getEntity());
获取到字符串bodyString =
"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"
因为我们只要value,所以我们很容易想到构建一个 Map集合
然后 resultMap =gson.fromJson(bodyString,HashMap.class);
String codeUrl = resultMap.get("code_url"); 我们到此就获取到这个二维码链接了.
一般我们还要把商品订单号也放入到这个 resultMap ,然后一并返回给controller中调用他的方法.
{"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"}
4.我们就把那个code_url变成二维码图片展示给用户.
如果你是html的话,一般就把这个code_url放到img中即可。
如果你是用vue的话,一般可以引入二维码模块, 如在packjson(相当于pom.xml)中导入"vue-qriously":"^1.1.1"
然后在main.js import引入
Vue.use()使用
然后就可以在index.vue中通过html的形式去引入二维码了。
比如<qriously :value="codeUrl" :size="300"/>
5.签名原理
我们可以在yml 配置文件中 配置 日志类型为 logging:level:root:debug,然后debug启动,
具体你看官方文档或者百度吧,孩子累了,毕竟是第二次写了。该死的ctrl+z
6.回调(重难点)
解释
图X2中的第10步。 由微信端请求我们的回调地址,给我们发送信息。
接口说明
适用对象: 直连商户
请求对象:POST
回调URL:该链接是通过基础下单接口中的请求参数“notify_url”来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。回调URL示例:“https://pay.weixin.qq.com/wxpay/pay.action”
通知规则
用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。
对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。
通知报文
支付结果通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情。
(注:由于涉及到回调加密和解密,商户必须先设置好apiv3秘钥后才能解密回调通知,这也就是我在图X1中说了一句话 对称加密对象 ,图X1中的APIV3 密钥加密解密apiV3Key.getBytes(StandardCharsets.UTF_8)。
重点来了:1.下面是对回调数据的处理.
先看官方文档的示例返回的数据,我们要把这json数据变成字符串先。
{ "id": "EV-2018022511223320873", "create_time": "2015-05-20T13:29:35+08:00", "resource_type": "encrypt-resource", "event_type": "TRANSACTION.SUCCESS", "summary": "支付成功", "resource": { "original_type": "transaction", "algorithm": "AEAD_AES_256_GCM", "ciphertext": "", "associated_data": "", "nonce": "" }}
1、先把request中的body数据拿到,转换成字符串。
有很多种方法,一种是常见的inputStream输入流,然后通过数组和输出流,读到-1就读完了。
一种是通过bufferedReader 和StringBuilder , br.readLine等等也可以实现。
总之是把request的数据变成字符串
第一种:
/** * 将通知参数转化为字符串 * @param request * @return */private String readData(HttpServletRequest request) { InputStream inStream = null; ByteArrayOutputStream outStream = null; String body = null; try { inStream = request.getInputStream(); outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inStream.read(buffer)) != -1) { outStream.write(buffer, 0, len); } body = new String(outStream.toByteArray(), "utf-8"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (null != inStream) { inStream.close(); } if (null != outStream) { outStream.close(); } } catch (IOException e) { e.printStackTrace(); } } return body; }
第二种:
/** * 将通知参数转化为字符串 * @param request * @return */ public static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
我们最好把那字符串变成Map集合,就方便拿出参数了。如
Map<String,Object> bodyMap = gson.toJson(body,HashMap.Class);
其中resource.ciphertext数据是加密的,我们需要把他解密才能拿到里面的信息.(下面的报文解密会说)
2.验证签名
对应图X1中的 第3步,异步回调() .因为步骤1和2 的验签和加签微信的SDK中有对应的工具类已经完成了。而对于步骤3,我们需要对微信的请求数据进行验证签名,同样的也有对应的工具类
官方文档给出的例子https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/blob/master/src/test/java/com/wechat/pay/contrib/apache/httpclient/NotificationHandlerTest.java#105
回调通知的验签与解密
版本>=0.4.2可使用 NotificationHandler.parse(request) 对回调通知验签和解密:
下面这几个参数,我们只需要从请求头中去获取就好了 ,比如
String serialNumber = request.getHeader("Wechatpay-Serial");
String nonce = request.getHeader("Wechatpay-Nonce");
String timestamp = request.getHeader("Wechatpay-Timestamp");
String signature = request.getHeader("Wechatpay-Signature");
HTTP/1.1 200 OKServer: nginxDate: Tue, 02 Apr 2019 12:59:40 GMTContent-Type: application/json; charset=utf-8Content-Length: 2204Connection: keep-aliveKeep-Alive: timeout=8Content-Language: zh-CNRequest-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8aWechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8cWechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==Wechatpay-Timestamp: 1554209980Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
使用NotificationRequest构造一个回调通知请求体。
需设置应答平台证书序列号、应答随机串、应答时间戳、应答签名串、应答主体。
使用NotificationHandler构造一个回调通知处理器。
需设置验证器(就是这一步,上面我们已经弄好了,拿来用就行了。
verifier = certificatesManager.getVerifier(mchId);),
apiV3密钥。调用parse(request)得到回调通知notification。
示例请参考下列代码。
// 构建request,传入必要参数 NotificationRequest Nrequest = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial) .withNonce(nonce) .withTimestamp(timestamp) .withSignature(signature) .withBody(body) .build();NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));// 验签和解析请求体Notification notification = handler.parse(Nrequest);// 从notification中获取解密报文System.out.println(notification.getDecryptData());
异常处理
parse(request)
可能返回以下异常,推荐对异常打日志或上报监控。
ValidationException
时,请先检查传入参数是否与回调通知参数一致。若一致,说明参数可能被恶意篡改导致验签失败。抛出ParseException
时,请先检查传入包体是否与回调通知包体一致。若一致,请检查AES密钥是否正确设置。若正确,说明包体可能被恶意篡改导致解析失败。
特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
3.报文解密
{ "id": "EV-2018022511223320873", "create_time": "2015-05-20T13:29:35+08:00", "resource_type": "encrypt-resource", "event_type": "TRANSACTION.SUCCESS", "summary": "支付成功", "resource": { "original_type": "transaction", "algorithm": "AEAD_AES_256_GCM", "ciphertext": "", "associated_data": "", "nonce": "" }}
前面我说过了resource.ciphertext中的数据是加密的,我们需要把他解密才能拿到里面的信息.
下面是解密后得到的部分信息,因为太多了。就展示一点
{ "transaction_id":"1217752501201407033233368018", "amount":{ "payer_total":100, "total":100, "currency":"CNY", "payer_currency":"CNY" }, "mchid":"1230000109", "trade_state":"SUCCESS", "bank_type":"CMC", }
// 从notification中获取解密报文
notification.getDecryptData()
下面来模仿一些简单的流程
@ResponseBody @RequestMapping(value = "/wxpay/notifyUrl", method = RequestMethod.POST) public String wxPayCallback(HttpServletRequest request, HttpServletResponse response) throws Exception { Gson gson =new Gson(); String body =WechatPayUtils.readData(request); String serialNumber = request.getHeader("Wechatpay-Serial"); String nonce = request.getHeader("Wechatpay-Nonce"); String timestamp = request.getHeader("Wechatpay-Timestamp"); String signature = request.getHeader("Wechatpay-Signature"); Map<String,String> map =new HashMap<>(); // 构建request,传入必要参数 NotificationRequest Nrequest = new NotificationRequest.Builder().withSerialNumber(serialNumber) .withNonce(nonce) .withTimestamp(timestamp) .withSignature(signature) .withBody(body) .build();//WechatPayUtils.getVerifier()就是获取上面的验签器,这只是我定义的一个方法来获取而已,你要怎样获取都行。构造验签器方法跟上面还是一模一样. NotificationHandler handler = new NotificationHandler(WechatPayUtils.getVerifier(), WechatPayConfig.v3Key.getBytes(StandardCharsets.UTF_8));//WechatPayConfig.v3Key 也就是APIV3key//JSON.parseObject,是将Json字符串转化为相应的对象;JSON.toJSONString则是将对象转化为Json字符串.用 Gson.toJson也行 try { // 验签和解析请求体 Notification notification = handler.parse(Nrequest); // 从notification中获取解密报文。 String plainText = notification.getDecryptData();//将密文转为map ,之后处理业务逻辑 Map resultMap =gson.fromJson(plainText,HashMap.class); log.info("验签成功"); if (wxpayService.payRecord(resultMap)) { response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); log.info("入库成功"); } //成功应答 response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "成功"); return gson.toJson(map); } catch (Exception e) { log.error("验签失败"); //应答失败 response.setStatus(500); map.put("code", "ERROR"); map.put("message", "验签失败"); return gson.toJson(map); } }
注意:证书序列号值并不是我们在yml中所配置的,微信会重新发送一个新的证书序列号放在请求头,我们必须用这个证书序列号去换取证书实例,换取公钥验签。
通知应答
接收成功:HTTP应答状态码需返回200或204,无需返回应答报文。
接收失败:HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,格式如下:
回状态码 | code | string[1,32] | 是 | 错误码,SUCCESS为清算机构接收成功,其他错误码为失败。 示例值:FAIL |
返回信息 | message | string[1,64] | 是 | 返回信息,如非空,为错误原因。 示例值:失败 |
上面其实已经可以对微信V3native支付中用了。
下面是V3APP支付,代码篇,通过代码来讲解,超详细,甚至可以直接拿代码复制即可用。微信V3APP代码篇拿来即用http://t.csdn.cn/au0qb