微信支付V3 超级详细版请认真看完——(第2集)

目录

关于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添加商户信息

// 从证书管理器中获取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,我们需要对微信的请求数据进行验证签名,同样的也有对应的工具类

官方文档给出的例子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 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,同时需返回应答报文,格式如下:

回状态码codestring[1,32]错误码,SUCCESS为清算机构接收成功,其他错误码为失败。
示例值:FAIL
返回信息messagestring[1,64]返回信息,如非空,为错误原因。
示例值:失败

 上面其实已经可以对微信V3native支付中用了。 

下面是V3APP支付,代码篇,通过代码来讲解,超详细,甚至可以直接拿代码复制即可用。微信V3APP代码篇拿来即用icon-default.png?t=M276http://t.csdn.cn/au0qb

  • 27
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值