微信支付签名与验签-APP支付问题

目录

使用API v3微信支付遇到的问题:

1.微信请求客户端配置

2.生成预付款订单

3.拼接字符串及使用私钥签名

4.微信支付成功后通知及使用公钥验签


使用API v3微信支付遇到的问题:

1.jdk版本问题, 自动更新证书加载失败, 改成jdk11解决;

2.请求头必须设置请求头 Accept, content-Type

3.通知接收 RequestBody 必须使用Object如果用实体类JSON化后属性位置可能发生变化, 影响验签, 不同的JSON类会影响转成字符串后的属性顺序, 我用的是 com.fasterxml.jackson.databind.ObjectMapper 用阿里的JSON类验签不行

4.支付通知本地调试: 配置nginx路由到本地机器, 就可以让微信支付宝直接通知到自己本地服务了

1.微信请求客户端配置

@Configuration
@Data
public class WxpayClientConfig extends WxpayConfig {

    private PrivateKey merchantPrivateKey;

    private Verifier verifier;

    @Bean(name = "wx_client")
    public HttpClient getClient() throws Exception {
        String merchantSerialNumber = super.getMchSerialNo();
        // 加载商户私钥(privateKey:私钥字符串)
        this.merchantPrivateKey = this.getPrivateKey(super.getPrivateKey());

        // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3秘钥)
        this.verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(super.getMerchantId(), new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
                super.getApiV3Key().getBytes("utf-8"));

        // 初始化httpClient
        Validator validator = new WechatPay2Validator(verifier);
        HttpClient httpClient = WechatPayHttpClientBuilder.create()
                .withMerchant(super.getMerchantId(), merchantSerialNumber, merchantPrivateKey)
                .withValidator(validator).build();
        return httpClient;
    }

    /**
     * 获取私钥。
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    private PrivateKey getPrivateKey(String filename) throws IOException {

        String content = new String(Files.readAllBytes(Paths.get(filename)), "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("无效的密钥格式");
        }
    }
}

2.生成预付款订单

下面代码中的httpClient为第1步里面注入的@Bean, 这一步请求会返回预付款ID: {
"prepay_id":"xxxxxxxxxxxxxxxxx"
}
       String outTradeNo = OrderUtil.getOrderId();
        final WxNativeBean wxNativeBean = new WxNativeBean();
        wxNativeBean.setAppid(super.getAppId());
        wxNativeBean.setMchid(super.getMerchantId());
        wxNativeBean.setNotify_url(super.getNotifyUrl());
        wxNativeBean.setDescription("充值"); //TODO
        wxNativeBean.setOut_trade_no(outTradeNo);
        WxNativeBean.Amount amount = new WxNativeBean.Amount();
        amount.setTotal((int) (Double.parseDouble(productModel.getTotalAmount()) * 100));
        wxNativeBean.setAmount(amount);

        final HttpPost httpPost = new HttpPost(super.getNativeUrl());
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
        String userAgent = String.format(
                "WechatPay-Apache-HttpClient/%s (%s) Java/%s",
                getClass().getPackage().getImplementationVersion(),
                System.getProperty("os.name") + "/" + System.getProperty("os.version"),
                System.getProperty("java.version"));
        httpPost.setHeader("User-Agent", userAgent);
        log.info("调用微信预支付请求参数: {}", wxNativeBean);
        httpPost.setEntity(new StringEntity(objectMapper.writeValueAsString(wxNativeBean), "UTF-8"));
        final HttpResponse response = httpClient.execute(httpPost);
        int code = response.getStatusLine().getStatusCode();
        log.info("微信native请求返回码: {}", code);
        final HttpEntity entity = response.getEntity();
        String t = EntityUtils.toString(entity);
        log.info("微信native请求返回内容: {}", t);

3.拼接字符串使用API v3签名

    按照需求拼接字符串, 直接copy微信的描述, 拼接好字符串后使用api v3秘钥进行签名,  第1步初始化是向外暴露了两个变量 :  一个是apiV3私钥对象, 一个是微信公钥证书, 这两个对象非常重要, 私钥对象用来签名, 公钥证书用来验签

private WxOrderInfo getOrderInfo(String prepayId) throws Exception {
        final WxOrderInfo wxOrderInfo = new WxOrderInfo();
        wxOrderInfo.setAppid(super.getAppId());
        wxOrderInfo.setPartnerid(super.getMerchantId());
        wxOrderInfo.setPrepayid(prepayId);
        wxOrderInfo.setTimestamp(System.currentTimeMillis() + "");
        wxOrderInfo.setNoncestr(OrderUtil.getOrderId());
        String content = wxpayClientConfig.getAppId() + "\n"
                + System.currentTimeMillis() / 1000 + "\n"
                + OrderUtil.getOrderId() + "\n"
                + prepayId + "\n";
        log.info("待签名字符串: {}", content);
        wxOrderInfo.setSign(SignUtil.sign(content, wxpayClientConfig.getMerchantPrivateKey()));
        return wxOrderInfo;
    }
public class SignUtil {

    private final static String algorithm = "SHA256withRSA";

    public static String sign(String content, PrivateKey privateKey) throws Exception {
        Assert.notNull(content, "签名内容不能为空");
        final Signature signature = Signature.getInstance(algorithm);
        signature.initSign(privateKey);
        signature.update(content.getBytes());
        return Base64.getEncoder().encodeToString(signature.sign());
    }
}

签名完成后把订单发给app调起微信支付

4.微信支付成功后通知

       注意, 通知地址必须是https的路径, 接收通知的参数最好用Object对象, 验签后再转为实体类取出参数入库, 成功后必须返回xml的字符串, 否则微信会一直通知

    @PostMapping("/wx/notice.html")
    public String wxpayCallback(HttpServletRequest request, HttpServletResponse response, @RequestBody Object wxNoticeModel) throws Exception {
        log.info("收到微信异步通知 =====> : {}", objectMapper.writeValueAsString(wxNoticeModel));
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        String nonce = request.getHeader("Wechatpay-Nonce");
        String sign = request.getHeader("Wechatpay-Signature");
        String serialNumber = request.getHeader("Wechatpay-Serial");
        wxpayService.verifyPay(timestamp, nonce, sign, serialNumber, wxNoticeModel);
        response.addHeader("Content-type", "text/xml");
        return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
    }

验签方法

 public void verifyPay(String timestamp, String nonce, String sign, String serialNumber, Object wxNoticeModel) throws Exception {
        String content = timestamp + "\n"
                + nonce + "\n"
                + objectMapper.writeValueAsString(wxNoticeModel) + "\n";
        if (!wxpayClientConfig.getVerifier().verify(serialNumber, content.getBytes(), sign)) {
            throw new Exception("微信支付校验不通过");
        }
        final WxNoticeModel.NoticeData resource = objectMapper.convertValue(wxNoticeModel, WxNoticeModel.class).getResource();
        String str = "";
        try {
            str = SignUtil.decryptToString(resource.getAssociated_data().getBytes(), resource.getNonce().getBytes(),
                    resource.getCiphertext(), super.getApiV3Key());
        } catch (Exception e) {
            log.error("解密微信通知参数错误", e);
            throw new Exception("解密微信通知参数错误");
        }
        Map<String, Object> map = objectMapper.readValue(str, Map.class);
        String outTradeNo = map.get("out_trade_no") + "";
        String status = map.get("trade_state") + "";
        log.info("通知订单号: {}, 订单状态: {}", outTradeNo, status);
    }

解密微信参数

 public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext, String key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
        GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, spec);
        cipher.updateAAD(associatedData);
        return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
    }

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值