从零玩转系列之微信支付实战PC端支付微信回调接口搭建1

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 从零玩转系列之微信支付实战PC端支付微信回调接口搭建

前言

接第四章从零玩转系列之微信支付实战PC端支付下单接口搭建后续.

此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端),至此微信支付Native支付完成.此篇文章过长我将分几个阶段的文章发布(项目源码都有,小程序和PC端)

在此之前已经更新了微信支付开篇、微信支付安全、微信实战基础框架搭建、本次更新为微信支付实战PC端接口搭建,实战篇分为几个章节因为代码量确实有点多哈.

  • 第一章从零玩转系列之微信支付开篇
  • 第二章从零玩转系列之微信支付安全
  • 第三章从零玩转系列之微信支付实战基础框架搭建
  • 第四章从零玩转系列之微信支付实战PC端支付下单接口搭建
  • 第五章从零玩转系列之微信支付实战PC端支付微信回调接口搭建
  • 第六章从零玩转系列之微信支付实战PC端支付微信取消订单接口搭建
  • 第七章从零玩转系列之微信支付实战PC端支付微信退款订单接口搭建
  • 第八章从零玩转系列之微信支付实战PC端项目构建Vue3+Vite+页面基础搭建
  • 第九章从零玩转系列之微信支付实战PC端装修下单页面
  • 第十章从零玩转系列之微信支付实战PC端装修我的订单下单页面
  • 第十一章从零玩转系列之微信支付实战PC端我的订单接入退款取消接口
  • 第十二章从零玩转系列之微信支付实战Uni—App基础项目搭建

a6875a95223c5902d9f4a9b18741b15.png

本次项目使用技术栈

后端: SpringBoot3.1.x、Mysql8.0、MybatisPlus

前端: Vue3、Vite、ElementPlus

小程序: Uniapp、Uview

Native模式回调

当用户支付完成时候微信会下发一个回调到我们系统当中

该链接是通过基础下单接口中的请求参数notify_url来设置的,要求必须为https地址。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到微信的回调通知信息。回调URL示例: “https://xxxxxx.com/api/wx-pay/native/notify

通知规则

用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。

对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m)

OK 我们在下单的时候设置了回调必须是HTTPS的SSL证书的

搭建本地调试 到时候上线的时候就替换域名即可

同学们可以使用免费的内网穿透,使用方式官方文档很详细仔细看我这就不讲解.

提供免费内网穿透服务,免费服务器支持绑定自定义域名

管理内网服务器,内网web进行演示

快速开发微信程序和第三方支付平台调试

本地WEB外网访问、本地开发微信、TCP端口转发

本站新增FRP服务器,基于 FRP 实现https、udp转发

无需任何配置,下载客户端之后直接一条命令让外网访问您的内网不再是距离

目前博主使用的是花生壳 收费也就6块钱 给了两个SSL的域名速度还可以

  • https://hsk.oray.com/ 花生壳🥜 so easy to happy的东西
  • 无需依赖公网IP、无需配置路由器,花生壳支持在客户端上
  • 添加端口映射,快速将内网服务发布到外网

73218042b847201f39466287efd077a.png

开启内网穿透代理地址到本地 127.0.0.1:9080

修改 wxpay.properties 当中 wxpay.notify-domain 参数为你的内网穿透地址

1d4b547b0991040faa2dd72f13d6b0e.png

支付通知

通知报文

支付结果通知是以POST 方法访问商户设置的通知url,通知的数据以JSON 格式通过请求主体(BODY)传输。通知的数据包括了加密的支付结果详情。

(注:由于涉及到回调加密和解密,商户必须先设置好apiv3秘钥后才能解密回调通知,apiv3秘钥设置文档指引详见APIv3秘钥设置指引)

上面的为商户APIV3的密钥之前我们已经设置好了还未设置的请参考开篇->获取APIv3秘钥(后续都是使用这个秘钥)

通知签名

加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。签名验证的算法请参考 《微信支付API v3签名验证》。

官方话语我就不说了感兴趣的去看文档详细的

总结一下回调需要干的事情

1.签名验证

处理签名验证

构造验签名串

首先,商户先从应答中获取以下信息。

  • HTTP头Wechatpay-Timestamp 中的应答时间戳。
  • HTTP头Wechatpay-Nonce 中的应答随机串。
  • 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。

然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。

应答时间戳\n
应答随机串\n
应答报文主体\n

我们可以看微信它是咋验证的我们就根据文档的要求改造一下子就行idea 按两下 shift 搜索 WechatPay2Validator

引用地址: com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator

6ea74011d7851d2f44d786c9b152132.png

好像啊 直接Copy 新增wechat文件夹复制到该文件夹当中 命名为 WechatPay2ValidatorForRequest

模仿微信验证签名,自定义支付通知API验证签名,针对通知请求的签名验证

改造构造函数

// 回调报文
protected final String body;
// 回调唯一ID 没啥用反正原来存在我们就放在这呗
protected final String requestId;
/**
 * 微信验证器
 *
 * @param verifier  验证器
 * @param requestId 请求id
 * @param body      微信回调的body
 */
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
    this.verifier = verifier;
    this.requestId = requestId;
    this.body = body;
}

改造验证方法

/**
 * 验证
 *
 * @param request 请求
 * @return boolean 是否成功
 * @throws IOException ioexception
 */
public final boolean validate(HttpServletRequest request) throws IOException {
    try {
        // 调用验证回调参数
        validateParameters(request);
        // 验签字符串
        String message = buildMessage(request);
        String serial = request.getHeader(WECHAT_PAY_SERIAL);
        // 签名
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        // 进行验证
        if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
            throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                    serial, message, signature, request.getHeader(REQUEST_ID));
        }
    } catch (IllegalArgumentException e) {
        log.warn(e.getMessage());
        return false;
    }
    return true;
}
    /**
     * 构建验证签名消息
     * 参考文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">参考文档</a>
     * <p>
     * 构造验签名串
     * 首先,商户先从应答中获取以下信息。
     * <p>
     * HTTP头Wechatpay-Timestamp 中的应答时间戳。
     * HTTP头Wechatpay-Nonce 中的应答随机串。
     * 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。
     * 然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。
     * 若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。
     * <p>
     ************************************
     * 应答时间戳\n
     * 应答随机串\n
     * 应答报文主体\n
     ************************************
     * <p>
     *
     * @param request 请求
     * @return {@link String}
     * @throws IOException ioexception
     */
    protected final String buildMessage(HttpServletRequest request) throws IOException {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }

改造验证回调参数

/**
 * 验证参数
 *
 * @param request 请求
 */
protected final void validateParameters(HttpServletRequest request) {
    // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
    String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
    // 这些头必须存在否则直接是伪造
    String header = null;
    for (String headerName : headers) {
        header = request.getHeader(headerName);
        if (header == null) {
            throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
        }
    }
    // 循环完毕直接默认被赋值是时间戳
    String timestampStr = header;
    try {
        // 验证过期应答
        Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
        // 拒绝过期应答
        if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
            throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
        }
    } catch (DateTimeException | NumberFormatException e) {
        throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
    }
}

完整代码 自定义验证签名器

package com.yby6.wechat;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
 * 模仿微信验证签名,自定义支付通知API验证签名,针对通知请求的签名验证
 *
 * @author Yang Shuai
 * Create By 2023/06/18
 */
public class WechatPay2ValidatorForRequest {
    protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
    /**
     * 应答超时时间,单位为分钟
     */
    protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    protected final Verifier verifier;
    protected final String body;
    protected final String requestId;
    /**
     * 微信验证器
     *
     * @param verifier  验证器
     * @param requestId 请求id
     * @param body      微信回调的body
     */
    public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
        this.verifier = verifier;
        this.requestId = requestId;
        this.body = body;
    }
    /**
     * 参数错误
     *
     * @param message 消息
     * @return {@link IllegalArgumentException}
     */
    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }
    /**
     * 验证失败
     *
     * @param message 消息
     * @return {@link IllegalArgumentException}
     */
    protected static IllegalArgumentException verifyFail(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }
    /**
     * 验证
     *
     * @param request 请求
     * @return boolean 是否成功
     * @throws IOException ioexception
     */
    public final boolean validate(HttpServletRequest request) throws IOException {
        try {
            // 调用验证回调参数
            validateParameters(request);
            // 验签字符串
            String message = buildMessage(request);
            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            // 签名
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
            // 进行验证
            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, request.getHeader(REQUEST_ID));
            }
        } catch (IllegalArgumentException e) {
            log.warn(e.getMessage());
            return false;
        }
        return true;
    }
    /**
     * 验证参数
     *
     * @param request 请求
     */
    protected final void validateParameters(HttpServletRequest request) {
        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
        // 这些头必须存在否则直接是伪造
        String header = null;
        for (String headerName : headers) {
            header = request.getHeader(headerName);
            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }
        // 循环完毕直接默认被赋值是时间戳
        String timestampStr = header;
        try {
            // 验证过期应答
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期应答
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }
    /**
     * 构建验证签名消息
     * 参考文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">参考文档</a>
     * <p>
     * 构造验签名串
     * 首先,商户先从应答中获取以下信息。
     * <p>
     * HTTP头Wechatpay-Timestamp 中的应答时间戳。
     * HTTP头Wechatpay-Nonce 中的应答随机串。
     * 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。
     * 然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。
     * 若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。
     * <p>
     ************************************
     * 应答时间戳\n
     * 应答随机串\n
     * 应答报文主体\n
     ************************************
     * <p>
     *
     * @param request 请求
     * @return {@link String}
     * @throws IOException ioexception
     */
    protected final String buildMessage(HttpServletRequest request) throws IOException {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }
}

处理报文解密

2.验证成功后解密加密的报文

参数解密

下面详细描述对通知数据进行解密的流程:

1、用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;

2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;

3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;

注: AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空字符串。

证书和回调报文解密

为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。本章节详细介绍了加密报文的格式,以及如何进行解密。

微信返回来的加密报文格式

AES-GCM是一种NIST标准的认证加密算法, 是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。它最广泛的应用是在TLS中。

证书和回调报文使用的加密密钥为APIv3密钥。

对于加密的数据,我们使用了一个独立的JSON对象来表示。为了方便阅读,示例做了Pretty格式化,并加入了注释。

{
 "original_type": "transaction", // 加密前的对象类型
 "algorithm": "AEAD_AES_256_GCM", // 加密算法
 // Base64编码后的密文
 "ciphertext": "...",
 // 加密使用的随机串初始化向量)
 "nonce": "...",
 // 附加数据包(可能为空)
 "associated_data": ""
}

⚠️ 加密的随机串,跟签名时使用的随机串没有任何关系,是不一样的。

解密

算法接口的细节,可以参考RFC 5116。

大部分编程语言(较新版本)都支持了AEAD_AES_256_GCM 。开发者可以参考下列的示例,了解如何使用您的编程语言实现解密。

我们引入的SDK已经有工具类直接用 com.wechat.pay.contrib.apache.httpclient.util.AesUtil

创建处理返回的 \n 的报文转json

package com.yby6.wechat;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
 * 用于解析微信支付回调的数据
 *
 * @author Yang Shuai
 * Create By 2023/06/14
 */
public class HttpUtils {
    /**
     * 分析数据
     *
     * @param request 请求
     * @return {@link Map}<{@link String}, {@link Object}>
     */
    public static Map<String, Object> analysisData(HttpServletRequest request) {
        String body = HttpUtils.readData(request);
        return JSONUtil.toBean(body, HashMap.class);
    }
    /**
     * 将通知参数转化为字符串
     */
    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();
                }
            }
        }
    }
}

知识点就这些了 实现一手

创建 nativeNotify 映射方法

/**
 * 支付通知->微信支付通过支付通知接口将用户支付成功消息通知给商户
 * 参考:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml">...</a>
 */
@PostMapping("/notify")
    public Map<String, String> nativeNotify(HttpServletRequest request, HttpServletResponse response) {
        log.info("接收到微信服务回调......");
        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = JSONUtil.toBean(body, HashMap.class);
            String requestId = (String) bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);
            // 签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if (!wechatPay2ValidatorForRequest.validate(request)) {
                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                return WechatRep.fail();
            }
            log.info("通知验签成功:{}", bodyMap);
            log.info("回调业务处理完毕");
            // 成功应答
            response.setStatus(200);
            return WechatRep.ok();
        } catch (Exception e) {
            log.error("处理微信回调失败:", e);
            // 失败应答
            response.setStatus(500);
            return WechatRep.fail();
        }
    }


相关文章
|
3月前
|
API 开发者
微信native支付对接案例详解
本文详细介绍了微信Native支付的对接流程,包括效果展示、产品介绍、接入前准备、开发指引、API列表、支付通知等,并强调了只有通过微信认证的服务号才能对接微信支付。每年需支付300元认证费用。
161 3
|
5月前
|
Web App开发 移动开发 前端开发
H5微信外支付(移动端浏览器)
H5微信外支付(移动端浏览器)
131 1
 H5微信外支付(移动端浏览器)
|
5月前
|
移动开发 安全 API
微信H5支付--微信JS-SDK支付--点金计划
本文详细介绍了微信H5支付和JS-SDK支付的原理、配置和开发流程,涵盖了H5支付在移动端浏览器外唤起微信支付的细节,以及JS-SDK支付在微信内置浏览器中完成支付的相关注意事项。文章还针对微信支付常见问题,提供了解决方案和代码示例。最后,文章深入解析了微信支付点金计划,包括商家小票的自定义开发、API接口以及支付成功后的页面展示逻辑,为开发者提供了完整的开发参考。
301 0
微信H5支付--微信JS-SDK支付--点金计划
|
5月前
|
JavaScript 小程序 开发者
uni-app开发实战:利用Vue混入(mixin)实现微信小程序全局分享功能,一键发送给朋友、分享到朋友圈、复制链接
uni-app开发实战:利用Vue混入(mixin)实现微信小程序全局分享功能,一键发送给朋友、分享到朋友圈、复制链接
853 0
|
21天前
|
自然语言处理 搜索推荐 小程序
微信公众号接口:解锁公众号开发的无限可能
微信公众号接口是微信官方提供的API,支持开发者通过编程与公众号交互,实现自动回复、消息管理、用户管理和数据分析等功能。本文深入探讨接口的定义、类型、优势及应用场景,如智能客服、内容分发、电商闭环等,并介绍开发流程和工具,帮助运营者提升用户体验和效率。未来,随着微信生态的发展,公众号接口将带来更多机遇,如小程序融合、AI应用等。
|
8天前
|
人工智能 自然语言处理 小程序
技术小白如何利用DeepSeek半小时开发微信小程序?
通过通义灵码的“AI程序员”功能,即使没有编程基础也能轻松创建小程序或网页。借助DeepSeek V3和R1满血版模型,用户只需用自然语言描述需求,就能自动生成代码并优化程序。例如,一个文科生仅通过描述需求就成功开发了一款记录日常活动的微信小程序。此外,通义灵码还提供智能问答模式,帮助用户解决开发中的各种问题,极大简化了开发流程,让普通人的开发体验更加顺畅。
技术小白如何利用DeepSeek半小时开发微信小程序?
|
1天前
|
小程序 JavaScript 前端开发
微信小程序开发全流程:从注册到上线的完整指南
这篇文章详细记录了微信小程序的完整开发到最终上线的每一个步骤。适合对小程序开发感兴趣的个人开发者或希望了解完整流程的学习者,涵盖了云开发、事件绑定、生命周期管理、组件使用等关键内容。
23 10
|
1月前
|
人工智能 开发框架 机器人
AstrBot:轻松将大模型接入QQ、微信等消息平台,打造多功能AI聊天机器人的开发框架,附详细教程
AstrBot 是一个开源的多平台聊天机器人及开发框架,支持多种大语言模型和消息平台,具备多轮对话、语音转文字等功能。
2907 15
AstrBot:轻松将大模型接入QQ、微信等消息平台,打造多功能AI聊天机器人的开发框架,附详细教程
|
5月前
|
JSON 小程序 JavaScript
uni-app开发微信小程序的报错[渲染层错误]排查及解决
uni-app开发微信小程序的报错[渲染层错误]排查及解决
1124 7
|
5月前
|
小程序 JavaScript 前端开发
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
992 1

热门文章

最新文章