微信支付V3回调 验签 注意事项 (踩坑记录)

一、验证签名时要认真阅读文档!!

点击跳转微信验签文档
点击跳转微信支付回调文档

1.取出请求头备用

在这里插入图片描述

2.先拿到微信平台证书

获取平台证书
注意事项

  1. 根据请求头 Wechatpay-Serial 的值在对应的数据结构中拿到对应的证书信息
  2. 如果没有对应的证书信息就要先获得平台证书,才能进行验签
  3. 要请求 获取平台证书接口 根据要求进行签名并设置请求头
  4. 获取后拿到返回结果,根据请求头 Wechatpay-Serial 的值进行匹配
  5. 得到匹配的证书数据后按照 报文解密 将内容解密
  6. 根据解密信息获得证书,存储到某种数据结构中,方便下次调用

3. 拿到其他的请求头

在这里插入图片描述
注意事项

这里有个大坑,不知道其他人是否遇到,获取应答主体时,我转JSON时顺序乱掉了;
这里顺序千万不能乱! 这里顺序千万不能乱! 这里顺序千万不能乱!

起初我按照 回调支付结果文档 中去设置顺序发现文档中排列的顺序与我接收的顺序不一样,效果如下:
微信文档中的:
在这里插入图片描述
在这里插入图片描述
我接收到的:
在这里插入图片描述

所以要以一定要以应答主体为准!!!
java中最好使用 request.getInputStream() 获取

4.开始验签

  1. 对验签名串信息进行整理
应答时间戳\n
应答随机串\n
应答报文主体\n
  1. 获取应答签名 Wechatpay-Signature 并进行Base64解密
  2. 使用刚刚获取的微信支付平台的证书对验签名串和签名进行SHA256 with RSA签名验证

二、附上部分代码

	/**
     * 得到微信应答签名
     *
     * @return
     */
    public boolean getWeChatPayReplySign(String serialNo, String weChatPayTimestamp, String weChatPayNonce, String body, String weChatPaySignature) {
        String str = Stream.of(weChatPayTimestamp, weChatPayNonce, body).collect(Collectors.joining("\n", "", "\n"));
        System.out.println(str);
        Signature sign = Signature.getInstance("SHA256withRSA");
		sign.initVerify(this.getCertificate(serialNo));
        sign.update(str.getBytes(StandardCharsets.UTF_8));
        // 解码后的字符串
        return sign.verify(Base64.decodeBase64(weChatPaySignature));
    }
	/**
     * 获取token
     *
     * @param method
     * @param url
     * @param nonceStr
     * @param body
     * @return
     */
    public String getToken(String method, String url, String nonceStr, String body) {
        long timestamp = System.currentTimeMillis() / 1000;
        String message = Stream.of(method, url, String.valueOf(timestamp), nonceStr, body).collect(Collectors.joining("\n", "", "\n"));
        String signature = getWeChatSign(message);
        return new StringBuilder("mchid=\"").append(wxConfig.getMchId()).append("\",")
                .append("nonce_str=\"").append(nonceStr).append("\",")
                .append("timestamp=\"").append(timestamp).append("\",")
                .append("serial_no=\"").append(wxConfig.getMchSerialNo()).append("\",")
                .append("signature=\"").append(signature).append("\"").toString();
    }
	/**
	 * 解密请求体
	 *
	 * @param dto
	 * @return
	 */
    public String decryptResponseBody(EncryptCertificateDTO dto) {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        SecretKeySpec key = new SecretKeySpec("ApiV3Key".getBytes("utf-8"), "AES");
        GCMParameterSpec spec = new GCMParameterSpec(128, dto.getNonce().getBytes());
        cipher.init(Cipher.DECRYPT_MODE, key, spec);
        cipher.updateAAD(dto.getAssociated_data().getBytes());
        return new String(cipher.doFinal(Base64.decodeBase64(dto.getCiphertext())), "utf-8");
    }

	/**
     * 得到证书
     *
     * @param serialNo
     * @return
     */
    private Certificate getCertificate(String serialNo) {
        if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(serialNo)) {
            createCertificates(serialNo);
        }
        return CERTIFICATE_MAP.get(serialNo);
    }

	/**
     * 创建证书
     *
     * @param serialNo
     */
    private void createCertificates(String serialNo) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        List<CertificateDTO> certificates = getCertificates();
        if (CollectionUtils.isEmpty(certificates)) {
            throw new IllegalArgumentException("证书获取失败");
        }
        CertificateDTO certificateDTO = certificates.stream().filter(item -> serialNo.equals(item.getSerial_no())).findFirst().orElseThrow(() -> new IllegalArgumentException("证书获取失败"));
        Date now = new Date();
        if (format.parse(certificateDTO.getEffective_time()).before(now) && format.parse(certificateDTO.getExpire_time()).after(new Date())) {
            // 解密数据
            String publicKey = decryptResponseBody(certificateDTO.getEncrypt_certificate());
            final CertificateFactory cf = CertificateFactory.getInstance("X509");
            ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
            Certificate certificate = cf.generateCertificate(inputStream);
            // 清理HashMap
            CERTIFICATE_MAP.clear();
            // 放入证书
            CERTIFICATE_MAP.put(serialNo, certificate);
        }
    }
	/**
	 * 获取微信平台证书
	 *
	 * @return
	 */
	private List<CertificateDTO> getCertificates() {
        HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
        httpGet.setHeader("Accept", "application/json");
        httpGet.setHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
        String authorization = getToken("GET", "/v3/certificates", IdUtil.fastSimpleUUID(), "");
        httpGet.setHeader("Authorization", WE_CHAT_PAY2 + authorization);
        HttpClient httpClient = HttpClients.custom().build();
        // 执行
        HttpResponse response = httpClient.execute(httpGet);
        if (response.getStatusLine().getStatusCode() == 200) {
            JSONObject entity = JSONUtil.parseObj(EntityUtils.toString(response.getEntity()));
            return JSONUtil.toList(entity.getStr("data"), CertificateDTO.class);
        }
        return null;
    }

总结

文档一定要仔细看好,否则太耽误开发时间了
最后文章有什么不正确的地方请及时联系我改正,谢谢!

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值