最新版V3微信支付流程和注意事项

一、业务介绍
1.支付流程

这是微信官方的流程图,简单介绍就是,用户点击支付->调用后台预支付接口->返回结果给前端->前端拉起支付->用户付款->微信官方进行回调->支付成功。

2.准备材料

(1)首先要有公众号或者小程序或者app等,其次还需要注册一个微信商户。

(2)要去微信支付平台申请证书、秘钥等信息。

3.参数介绍

appId:小程序支付要使用,是小程序的appId。

appSecret:小程序支付使用的,是小程序的秘钥,生成后不可查看,记得保存。

merId:商家的商户号。

merchNumber:证书序列号。

apiV3Key:apiV3的秘钥。

3.注意事项

微信从24年8月份开始逐步取消平台私钥证书签名,新注册的商户号需要使用自己的公钥进行签名。

二、环境准备

微信支付分为V2和V3,本文主要介绍的是V3的支付流程。

项目采用SpringBoot。

1.设置开发参数

可以直接在yml文件中设置开发需要的参数

2.配置配置类
@Data
@Component
@ConfigurationProperties(prefix = "wx")
public class WxPayConfig {
    //小程序公众号的appId
    private String appId;
    //小程序公众号的秘钥
    private String appSecret;
    //回调地址
    private String notifyUrl;
    //商家商户号
    private String merchantId;
    //私钥地址
    private String privateKeyPath;
    //公钥地址
    private String publicKeyPath;
    //证书序列号
    private String merchantSerialNumber;
    //apiv3的秘钥
    private String apiV3Key;
​
}
二、使用官方sdk的方法
1.导入依赖
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.14</version>
</dependency>
2.预支付代码,这里用js支付举例
/**
 * 预支付接口
 */
public void prepay(){
    //更新证书配置,这里用官方sdk,不需要考虑时效性问题
    Config config = new RSAPublicKeyConfig.Builder()
            .merchantId(wxPayConfig.getMerchantId())
            .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
            .publicKeyFromPath(wxPayConfig.getPublicKeyPath())
            .publicKeyId(wxPayConfig.getPublicKeyPath())
            .merchantSerialNumber(wxPayConfig.getMerchantSerialNumber())
            .apiV3Key(wxPayConfig.getApiV3Key())
            .build();
    //初始化service
    JsapiService service = new JsapiService.Builder().config(config).build();
    PrepayRequest request = new PrepayRequest();
    request.setAppid(wxPayConfig.getAppId());
    request.setMchid(wxPayConfig.getMerchantId());
    request.setDescription("测试商品");
    request.setOutTradeNo("orderId");
    request.setNotifyUrl(wxPayConfig.getNotifyUrl());
    log.info("支付回调地址:{}",wxPayConfig.getNotifyUrl());
    Amount amount = new Amount();
    amount.setTotal(1);
    request.setAmount(amount);
    Payer payer = new Payer();
    request.setPayer(payer);
    //调用预支付接口
    PrepayResponse prepay = service.prepay(request);

预支付完成后会产生一个prepayId,返回给前端之后,前端根据返回参数拉起支付

3.前端签名

前端拉起支付有个参数是paySign,实际操作中前端不是很好处理,所以这里可以选择帮助前端进行签名。

public Map<String, String> getSign(String openId,Integer amount,String orderId) throws NoSuchAlgorithmException, IOException, InvalidKeyException, SignatureException {
    //生成32位的随机字符串
    String nonceStr = UUID.randomUUID().toString().replace("-", "");
    long timestamp = System.currentTimeMillis() / 1000;
    //prepay就是预支付代码,获取到prepayId
    String prepayId = prepay(openId,amount,orderId);
    String message = buildMessage(wxConfigVo.getAppId(), timestamp, nonceStr, prepayId);
    Signature sign = Signature.getInstance(SHA256WITHRSA);
    sign.initSign(getPrivateKey(wxConfigVo.getPrivateKeyPath()));
    sign.update(message.getBytes(StandardCharsets.UTF_8));
    String signature = Base64.getEncoder().encodeToString(sign.sign());
    HashMap<String, String> map = new HashMap<>();
    map.put("sign",signature);
    map.put("nonceStr",nonceStr);
    map.put("timestamp",timestamp+"");
    map.put("prepayId",prepayId);
    return map;
}
​
/**
 * 拼接需要的信息
 * @param appid
 * @param timestamp
 * @param nonceStr
 * @param prepay_id
 * @return
 */
String buildMessage(String appid, long timestamp,String nonceStr,String prepay_id) {
    return appid + "\n"
            + timestamp + "\n"
            + nonceStr + "\n"
            + "prepay_id="+prepay_id + "\n";
}
​
/**
 * 获取私钥文件
 * @param filename
 * @return
 * @throws IOException
 */
public PrivateKey getPrivateKey(String filename) throws IOException {
​
    String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8);
    try {
        String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");
​
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(
                new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("当前Java环境不支持RSA", e);
    } catch (InvalidKeySpecException e) {
        throw new RuntimeException("无效的密钥格式");
    }
}

在这里,前端也是需要我们签名产生的时间戳,随机字符串这些信息,所以选择返回一个map格式,里面都是重要信息。当然这一步选做,如果不想处理也可以选择让前端处理。这里只是列出方法,方便学习。

4.支付回调

拉起支付之后,用户选择付款之后,微信官方会发来一条支付完成的信息,里面包含是否支付成功等信息。

/**
 * 支付回调
 * @param request
 * @param response
 * @return
 */
public Transaction notifyPay(HttpServletRequest request, HttpServletResponse response) {
    String signature = request.getHeader("Wechatpay-Signature");
    String serial = request.getHeader("Wechatpay-Serial");
    String nonce = request.getHeader("Wechatpay-Nonce");
    String timestamp = request.getHeader("Wechatpay-Timestamp");
​
    RequestParam requestParam = new RequestParam.Builder()
            .serialNumber(serial)
            .nonce(nonce)
            .signature(signature)
            .timestamp(timestamp)
            .body(getRequestBody(request))
            .build();
    NotificationConfig config = new RSAPublicKeyConfig.Builder()
            .merchantId(wxConfigVo.getMerchantId())
            .privateKeyFromPath(wxConfigVo.getPrivateKeyPath())
            .publicKeyFromPath(wxConfigVo.getPublicKeyPath())
            .publicKeyId(wxConfigVo.getPublicKey())
            .merchantSerialNumber(wxConfigVo.getMerchantSerialNumber())
            .apiV3Key(wxConfigVo.getApiV3Key())
            .build();
    NotificationParser parser = new NotificationParser(config);
    Transaction transaction = parser.parse(requestParam, Transaction.class);
    return transaction;
}
​
private String getRequestBody(HttpServletRequest request) {
​
    StringBuilder sb = new StringBuilder();
​
    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) {
​
    }
​
    return sb.toString();
​
}
5.注意事项

(1)在做支付的时候,遇到过一些问题,比如在加密解密的过程中,因为jdk版本过低导致加解密失败,这里可以去百度一下,替换一下jre下的security包下的两个包就可以解决,低版本最高支持128位加密,微信现在是256位的。

(2)新申请的商户号要用公钥签名,用公钥,用公钥。

(3)多看微信官方文档,这里只是列出了代码,具体的参数和签名,解密这些没有具体说明,后续会单独写一篇文章介绍所有加密解密,验签的方法和原理。

三、不使用官方sdk的方法

不是很推荐这样自己写代码,很复杂,而且容易出错,不过给爱研究的人看一下具体过程,这里只展示如何进行签名,具体调接口和参数根据自己实际业务来进行操作。

@Component
public class WechatUtil {
​
    @Autowired
    private WxPayConfig config;
​
    private static String SHA256WITHRSA = "SHA256withRSA";
​
    private String mchId= config.getMerchantId();
​
    private String certificateSerialNo=config.getMerchantSerialNumber();
​
    /**
     * body是自己的支付信息,类似于{"appid":"wxd678efh567hg6787","mchid":"1230000109","description":"Image形象店-深圳腾大-QQ公仔","out_trade_no":"1217752501201407033233368018","notify_url":"https://www.weixin.qq.com/wxpay/pay.php","amount":{"total":100,"currency":"CNY"},"payer":{"openid":"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"}}
     * @param body
     * @return
     */
    public Map<String,String> getSignMap(String body) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        Map<String,String> map = new HashMap<>();
        map.put("Authorization",getSign(body));
        map.put("Content-Type","application/json");
        map.put("Accept","application/json");
        return map;
    }
​
    private String getSign(String body) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        return "WECHATPAY2-SHA256-RSA2048 "+getToken(body);
    }
​
    private String getToken(String body) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        String timestamp = System.currentTimeMillis() / 1000+"";
        String message = buildMsg(timestamp,nonceStr,body);
        Signature signature = Signature.getInstance(SHA256WITHRSA);
        signature.initSign(getPrivateKey("自己文件路径"));
        signature.update(message.getBytes(StandardCharsets.UTF_8));
        String sign = Base64.getEncoder().encodeToString(signature.sign());
        return "mchid=\"" + mchId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + certificateSerialNo + "\","
                + "signature=\"" + sign + "\"";
    }
​
    private String buildMsg(String timestamp, String nonceStr, String body) {
        return "POST"+"\n"
                +"/v3/certificates"+"\n"
                +timestamp+"\n"
                +nonceStr+"\n"
                +body+"\n";
    }
​
    /**
     * 获取私钥文件
     * @param filename
     * @return
     * @throws IOException
     */
    public PrivateKey getPrivateKey(String filename) throws IOException {
​
        String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8);
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");
​
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

第一个方法返回值就是微信拉起支付的请求头,body参数根据自己的业务来产生。

@RequestMapping(value = "/test", method = RequestMethod.POST, produces = "application/json; charset=utf-8")
@ApiOperation("测试")
public void test() throws Exception{
        //处理请求参数
        String param = JSON.toJSONString("请求参数");
        //获取签名请求头
        HashMap<String, String> heads = WechatUtil.getSignMap(param);
        //请求微信接口
        HttpUtils.requestPostBody("微信接口url", param, heads);
    }

以上就是两种实现支付的方法,支付还需要考虑到很多业务问题,比如如何保证幂等性,回调如果失效需要主动查询微信方等等。关于RSA的内容和如何获取微信各种参数,申请证书这些后面会单独写一篇文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值