小程序微信支付后端实现(Java)

一、前言

微信小程序应用接入微信支付,java后端实现。

接入前需要准备相关的参数小程序id(即appId),商户号(mchId),apiV3Key,微信针对此商户颁发的公私钥(apiclient_key.pem,apiclient_cert.pem),商户序列号merchantSerialNumber

二、签名和验签

微信支付的相关接口都需要签名。

这里简单解释一下签名和验签。

详细可以参考juejin.cn/post/684490...

1、签名

就如同我们写信的时候签名一样,证明这封信是我本人写的所以需要签名,所以一般是使用私钥。签名的流程一般是对请求体先进行摘要,这个一般是不可逆,通过散列函数等方式,然后拿私钥通过某种算法加密一下就得到了签名值。

2、验签

我们一般是将签名值和请求体数据一块发给服务方,服务方收到请求之后,先对请求体数据进行摘要(散列函数)得数据A,然后对签名值使用公钥解密得到数据B,然后对比数据A和数据B是否一致,如此来保证请求的数据没有被篡改。

三、举例

我们可以使用微信官方的一段例子。pay.weixin.qq.com/docs/mercha...

微信官方的一个接口api.mch.weixin.qq.com/v3/certific...

先不用管这个接口是干嘛用的,我们正常把接口放到postman请求,很明显是请求失败的。

然后我们按照官方给出的格式,对数据进行签名,得到签名值之后按照他们官方得格式,拼接得到一个认证值,再去请求接口就能正常请求得通了。

获取签名值得相关代码如下

java 复制代码
package com.example;
​
import com.wechat.pay.java.core.util.PemUtil;
import okhttp3.HttpUrl;
import org.junit.jupiter.api.Test;
​
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
​
​
public class SignTest {
    private String url = "https://api.mch.weixin.qq.com/v3/certificates";
    // 官方默认得前缀
    private String schema = "WECHATPAY2-SHA256-RSA2048";
    private HttpUrl httpurl = HttpUrl.parse(url);
​
​
    @Test
    public void getToken( ) throws Exception {
        String nonceStr = "aassccgvd";
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage("GET", httpurl, timestamp, nonceStr, "");
​
        // 得到签名值
        String signature = sign(message.getBytes(StandardCharsets.UTF_8));
        // 认证值需要拼上自己得商户号和商户序列号
        String result =  schema+" mchid="" + "xxxxx" + "","
                + "nonce_str="" + nonceStr + "","
                + "timestamp="" + timestamp + "","
                + "serial_no="" + "xxxxxx" + "","
                + "signature="" + signature + """;
        System.out.println(result);
    }
​
    public String sign(byte[] message) throws Exception {
        Signature sign = Signature.getInstance("SHA256withRSA");
        // 使用私钥进行加密  
        sign.initSign( PemUtil.loadPrivateKeyFromPath("./apiclient_key.pem"));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }
​
    public String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }
        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }
}
​

上述代码中PemUtil.loadPrivateKeyFromPath()使用了微信官方SDK中的方法,这里就是根据配置的路径读取文件流,获取文件里面的字符串,然后进行截取,获得私钥串,最后返回PrivateKey对象,这里截图看一下源码,这里大家可以自己写。

四、微信支付

现在我们切回正题,讲一下微信支付的业务流程,我先贴一张微信方法图片。

由图片可以看出,消费者在你的小程序下单某一件商品之后,你自己的商户系统会生成一个订单,然后你的商户系统去调用微信官方的下单接口(这里的下单意思就是需要发起微信支付了,在微信那边下一个单,注意与商户自己的订单的区别),微信官方的接口会给你返一个预支付id(prepayId),然后你把这个预支付id丢给前端,前端会去调微信的组件,拉起支付,组件底层应该也是调用了微信的相关接口,然后完成支付。微信支付完成之后,会调用你系统的回调接口,系统接口需要时https,(本地调式的时候可以开一个https内网穿透),回调之后,商户系统就可以将订单置为已支付。

由上述我们知道,微信支付的接口需要签名和验签,相关接口属实复杂,所以我们可以直接使用微信官方提供的相关SDK,该SDK帮我们省去了签名和验签的繁琐步骤。

xml 复制代码
<dependency>  
    <groupId>com.github.wechatpay-apiv3</groupId>  
    <artifactId>wechatpay-java</artifactId>  
    <version>0.2.12</version>  
</dependency>

大家可以去github查找最近的版本。

使用方式也很简单,先往Ioc容器放一个配置对象

kotlin 复制代码
package com.example.wechat.official.config;
​
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
public class WxPayConfig {
​
​
    @Value("${wx.pay.appId}")
    private String appId;
​
    @Value("${wx.pay.mchId}")
    private String mchId;
​
    @Value("${wx.pay.privateKeyPath}")
    private String privateKeyPath;
​
    @Value("${wx.pay.merchantSerialNumber}")
    private String merchantSerialNumber;
​
​
    @Value("${wx.pay.apiV3Key}")
    private String apiV3Key;
​
    @Bean
    public Config setWxPayConfig() {
        Config config =
                new RSAAutoCertificateConfig.Builder()
                        .merchantId(mchId)
                        .privateKeyFromPath(privateKeyPath)
                        .merchantSerialNumber(merchantSerialNumber)
                        .apiV3Key(apiV3Key)
                        .build();
        return config;
    }
​
}
​

然后就可以按照官方的接口文档塞参数请求接口了,(官方文档pay.weixin.qq.com/docs/mercha...) 注意我这里使用的是SDK中jsapi包下面的service和model,微信官方提供了好几种不同的支付,而不同的支付使用的是不同包下面的service和model

附上相关代码

scss 复制代码
@RestController
@RequestMapping("/test/pay")
@Slf4j
public class OfficialAppletPayController {
​
​
​
    @Value("${wx.pay.appId}")
    private String appId;
​
    @Value("${wx.pay.mchId}")
    private String mchId;
​
    @Autowired
    private Config config;
​
    @PostMapping("/createOrder")
    public Object createOrder() {
​
        // 构建service
        JsapiService service = new JsapiService.Builder().config(config).build();
        // request.setXxx(val)设置所需参数,具体参数可见Request定义
        PrepayRequest request = new PrepayRequest();
        request.setAppid(appId);
        request.setMchid(mchId);
        request.setDescription("测试商品标题");
        // 回调接口url
        request.setNotifyUrl("https://notify_url");
        // 商户系统自己的订单号,这里先随机生成,大家可以根据自己的系统生成规则生成订单号,
        // 要保证在自己的系统中唯一就行。
        String orderNo = RandomStringUtils.random(20, true, true).toUpperCase();
        request.setOutTradeNo(orderNo);
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 15);
        Date time = calendar.getTime();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
        String expiredTime = sdf.format(time);
        request.setTimeExpire(expiredTime);
​
        // 设置openid 相当于该微信用户的一个标识
        Payer payer = new Payer();
        payer.setOpenid("xxxxxxxxxxxxxxx");
        request.setPayer(payer);
​
​       // 请求付钱的金额,以分为单位
        Amount amount = new Amount();
        amount.setTotal(1);
        amount.setCurrency("CNY");
        request.setAmount(amount);
​
  
​
        // 调用下单方法,得到应答
        PrepayResponse response = service.prepay(request);
        System.out.println(response);
​
​
        return response;
​
    }
}

这里我们按照官方的接口文档,把必填项值塞一下,就可以正常请求得到prepayId,当然,请求的时候还是需要签名的,只不是这些工作被SDK帮忙做了。我们可以看一下SDK底层的相关源码。

这里getSchema()其实就是返回上述的默认值"WECHATPAY2-SHA256-RSA2048"

五、拉起支付

后端生成prepayId之后,并不能直接把这个prepayId给前端去调用微信的组件,因为微信支付组件底层还是要调微信的接口还是要签名的,所以一般是后端再对prepayId做一次签名,然后将该请求体返回前端,前端拉起支付,完成付款。

六、微信回调

消费者再付完钱之后,就会回调商户系统的接口(下单时候配置的notifyUrl,一般做成配置项),我们这里简称回调接口,商户系统可以在这个接口里面对商户的订单置为已完成态。

代码如下

java 复制代码
package com.example.wechat.official.controller;
​
​
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction;
import org.apache.commons.lang3.RandomStringUtils;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.model.Amount;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse;
import com.wechat.pay.java.service.payments.jsapi.model.Payer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
​
​
/**
 *  微信官方指定SDK--小程序支付
 */
@RestController
@RequestMapping("/test/pay")
@Slf4j
public class OfficialAppletPayController {
​
​
​
    @Value("${wx.pay.appId}")
    private String appId;
​
    @Value("${wx.pay.mchId}")
    private String mchId;
​
    @Autowired
    private Config config;
​
​
​
    @PostMapping("/notify")
    public Map wechatNotify(HttpServletRequest request) {
​
        String requestBody = getRequestBody(request).trim();
        log.info("支付结果异步接口,请求参数[转换前]:{}", requestBody);
        requestBody = notificationAttrRearrangement(requestBody);
​
        log.info("支付结果异步接口,请求参数[转换后]:{}", requestBody);
​
        // 构造 RequestParam
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(request.getHeader("Wechatpay-Serial"))
                .nonce(request.getHeader("Wechatpay-Nonce"))
                .signature(request.getHeader("Wechatpay-Signature"))
                .timestamp(request.getHeader("Wechatpay-Timestamp"))
                .body(requestBody)
                .build();
​
        // 如果已经初始化了 RSAAutoCertificateConfig,可直接使用
        // 初始化 NotificationParser
        NotificationParser parser = new NotificationParser((NotificationConfig)config);
        Transaction transaction;
        try {
            // 以支付通知回调为例,验签、解密并转换成 Transaction
            transaction = parser.parse(requestParam, Transaction.class);
​
        } catch (ValidationException e) {
            // 签名验证失败,返回 401 UNAUTHORIZED 状态码
            log.error("sign verification failed", e);
            return failResponse();
        }
        if (Objects.nonNull(transaction)) {
            try {
                // 商户自己的业务逻辑(将商户自己的订单号置为已支付状态等等)
            } catch (Exception e) {
                log.error("complete intentionOrder failed", e);
                return failResponse();
            }
            // 处理成功,返回 200 OK 状态码
            return sucessResponse();
        }
        // 如果处理失败,应返回 4xx/5xx 的状态码,例如 500 INTERNAL_SERVER_ERROR
        return failResponse();
    }
​
    private Map sucessResponse() {
        HashMap<String, String> resultMap = new HashMap<>();
        resultMap.put("code","SUCCESS");
        return resultMap;
    }
​
    private Map failResponse() {
        HashMap<String, String> resultMap = new HashMap<>();
        resultMap.put("code","FAIL");
        return resultMap;
    }
​
    /**
     * 获取请求头数据
     */
    private String getRequestBody(HttpServletRequest request) {
        StringBuffer sb = new StringBuffer();
        try (ServletInputStream inputStream = request.getInputStream();
             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        ) {
            String line;
            while ((line = reader.readLine()) != null) sb.append(line);
        } catch (IOException e) {
            log.error("读取数据流异常:{}", e);
        }
        return sb.toString();
    }
​
​
    /**
     * 对字段重排序用以符合签名校验
     * @param requestBody /
     * @return /
     */
    private String notificationAttrRearrangement(String requestBody) {
        JSONObject jsonObject = JSON.parseObject(requestBody);
        final String id = jsonObject.getString("id");
        final String createTime = jsonObject.getString("create_time");
        final String eventType = jsonObject.getString("event_type");
        final String resourceType = jsonObject.getString("resource_type");
        final String summary = jsonObject.getString("summary");
        JSONObject resource = jsonObject.getJSONObject("resource");
        String algorithm = resource.getString("algorithm");
        String ciphertext = resource.getString("ciphertext");
        String associatedData = resource.getString("associated_data");
        String originalType = resource.getString("original_type");
        String nonce = resource.getString("nonce");
        return "{" +
                ""id":"" + id + ""," +
                ""create_time":"" + createTime + ""," +
                ""resource_type":"" + resourceType + ""," +
                ""event_type":"" + eventType + ""," +
                ""summary":"" + summary + ""," +
                ""resource":{" +
                ""original_type":"" + originalType + ""," +
                ""algorithm":"" + algorithm + ""," +
                ""ciphertext":"" + ciphertext + ""," +
                ""associated_data":"" + associatedData + ""," +
                ""nonce":"" + nonce + """ +
                "}" +
                "}";
    }
​
}
​

这个时候我们属于是被调用方了,所以这个时候按照微信官方的建议,我们需要对这个回调使用公钥验签了,当时验签的过程SDK也帮我们完成了,验完签之后,还要对数据解密,得到Transaction对象,该对象的结构可以参考微信官方文档(pay.weixin.qq.com/docs/mercha...%25E3%2580%2582 "https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/payment-notice.html)%E3%80%82")

这里我们一起走读一下验签,解密的相关源码。 入口是是方法parser.parse(requestParam, Transaction.class)

再看一下解析报文的相关代码

七、最后

此外,市面上面还有一些个人写其他SDK,也是比较好用的,比较经典的比如

xml 复制代码
<dependency>  
    <groupId>com.github.binarywang</groupId>  
    <artifactId>weixin-java-pay</artifactId>  
    <version>4.5.0</version>  
</dependency>

这个SDK下单接口里面会将返回的prepayId做一下签名,就不需要我们自己手动再去签名了。这里也奉赠源码截图。

其中签名的数据体的格式得按照微信官方得来

至此,微信支付的所有流程都结束了,当然这里还可能涉及到商户系统去查询微信官方的订单情况,取消微信官方的订单等等,相关接口比较简单,SDK中对应的也都封装好了,大家有兴趣可以自行查看。然后有什么问题,不正之处或者建议,大家可以留言讨论。

原创不易,转载请贴出处。

相关推荐
代码之光_198028 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi34 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
丁总学Java1 小时前
微信小程序,点击bindtap事件后,没有跳转到详情页,有可能是app.json中没有正确配置页面路径
微信小程序·小程序·json
颜淡慕潇1 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
mosen8682 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
qq22951165023 小时前
微信小程序的汽车维修预约管理系统
微信小程序·小程序·汽车
monkey_meng4 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马4 小时前
Rust-Trait 特征编程
开发语言·后端·rust