目录
1、生成订单(当用户确认下单后,在后台系统,生成一个订单,此时还未支付呢。举个栗子:比如你在拼多多下单,是不是点击购买后,会有一个订单,然后显示你在10分钟之内支付。)
3.调用API之后,微信会返回结果也就是上面的response,native支付就会返回一个code_url
关于微信支付需要的基本配置,以及数字签名等,已在第一篇文章讲解,下面是开始代码篇。
关于API V3和V2的区别 (超级重要)
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添加商户信息
// 从证书管理器中获取verifier
verifier = 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的流程
//请求微信支付接口
上面的定时更新平台证书功能第一行我解释了。我们现在注入方法
@Resource private 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,我们需要对微信的请求数据进行验证签名,同样的也有对应的工具类
回调通知的验签与解密
版本>=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 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980
Wechatpay-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