在线支付系列【13】微信支付之签名验签流程分析

news2024/11/17 0:31:44

有道无术,术尚可求,有术无道,止于术。

文章目录

    • 前言
    • 签名生成
    • 签名验证
    • 总结

前言

在上篇文档中,我们简单实现了对接微信支付的几个接口。了解到wechatpay-apache-httpclient框架自动实现了签名和验签,接下来跟踪下源码,了解下具体流程~

签名生成

微信支付API v3 要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3将会拒绝处理请求,并返回401 Unauthorized

HttpClient执行下单请求时打入断点。

CloseableHttpResponse response = httpClient.execute(httpPost);

httpPost包含了请求路径、请求头、请求体等内容。
在这里插入图片描述
经过HttpClient自身框架的一些处理,进入到请求执行器RetryExec执行方法中,
在这里插入图片描述

接着调用请求执行器(微信SDKSignatureExec类)执行,会判断请求的主机地址是否以.mch.weixin.qq.com结尾,执行不同逻辑。

    public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request, HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException {
    	// api.mch.weixin.qq.com
    	// 是否已`.mch.weixin.qq.com`结尾
        return request.getTarget().getHostName().endsWith(".mch.weixin.qq.com") ? this.executeWithSignature(route, request, context, execAware) : this.mainExec.execute(route, request, context, execAware);
    }

是以.mch.weixin.qq.com结尾的请求,则进入到执行并签名的executeWithSignature方法中,在这里,会生成Authorization消息头。getSchema()返回的是固定常量WECHATPAY2-SHA256-RSA2048getToken(request)会根据请求生成签名。

request.addHeader("Authorization", this.credentials.getSchema() + " " + this.credentials.getToken(request));

getToken方法执行如下:

    public final String getToken(HttpRequestWrapper request) throws IOException {
    	// 1. 生成随机字符串,微信支付API接口协议中包含字段nonce_str,主要保证签名不可预测。
    	// 我们推荐生成随机数算法如下:调用随机数函数生成,将得到的值转换为字符串。
        String nonceStr = this.generateNonceStr();
        // 2. 获取发起请求时的系统当前时间戳,即格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数,
        // 作为请求时间戳。微信支付会拒绝处理很久之前发起的请求,请商户保持自身系统的时间准确。
        long timestamp = this.generateTimestamp();
        // 3. 将以上信息和请求参数,转为一个字符串,eg:POST /v3/pay/transactions/native .............
        String message = this.buildMessage(nonceStr, timestamp, request);
        log.debug("authorization message=[{}]", message);
        // 4. 私钥签名
        Signer.SignatureResult signature = this.signer.sign(message.getBytes(StandardCharsets.UTF_8));
        // 5. 拼接字符串
        String token = "mchid=\"" + this.getMerchantId() + "\",nonce_str=\"" + nonceStr + "\",timestamp=\"" + timestamp + "\",serial_no=\"" + signature.certificateSerialNumber + "\",signature=\"" + signature.sign + "\"";
        log.debug("authorization token=[{}]", token);
        return token;
    }

在签名方法sign中,进行签名。PS:不了解签名的参考数字签名。

    public Signer.SignatureResult sign(byte[] message) {
        try {
        	// 使用商户API证书下发时给的私钥进行签名
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(this.privateKey);
            sign.update(message);
            return new Signer.SignatureResult(Base64.getEncoder().encodeToString(sign.sign()), this.certificateSerialNumber);
        } catch (NoSuchAlgorithmException var3) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", var3);
        } catch (SignatureException var4) {
            throw new RuntimeException("签名计算失败", var4);
        } catch (InvalidKeyException var5) {
            throw new RuntimeException("无效的私钥", var5);
        }
    }

微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成。最终生成的Authorization消息头对应的内容如下:

// 认证类型,目前为WECHATPAY2-SHA256-RSA2048
WECHATPAY2-SHA256-RSA2048 
// 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
mchid="xxxxxxxx",
// 请求随机串nonce_str
nonce_str="7Sw2kxfrNWAlOzxxxxxxxxQ",
// 时间戳,Http头Authorization中的timestamp与发起请求的时间不得超过5分钟
timestamp="1675252352",
// 商户API证书序列号serial_no,用于声明所使用的证书
serial_no="34345964330B6xxxxxxxxF",
// 签名数据
signature="J6Pom6sWb1UPTK+Zg8+2x18S38hG4sJRMe6Lo6MU1nJNqYzuqcCxC7hBaGcjoloQqNQBoTdefoCATMPXvNOhzkLcABXG8rhBQ91fZSZxh+SpJY6x1shFyN3XMdmyE7ToQzImeV4KuSPcvPlO0waeQVN2K/ioZ+uaDdj/dxVk4RSkJ+eqTqQtt3T3bRN1+v11owNsJAekyNnqMnBL1LaExlqAD/iEYDQyfS+zarkPoKhvPxI23w3mWTHjQsoxkhdOCi4CO0i2QXVVKio9HNYZyqtTLbFq0S/azkF6LB6ZzWRIapIN5bG85kuzATdrh4T0abm53UTVn+TO1vGH9InKiw=="

最终交给HttpClient原生框架去执行,微信服务器收到请求消息后,使用商户API证书中公钥进行验签。

签名验证

如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

请求成功后,响应回来的信息如下:
在这里插入图片描述
在执行并签名的SignatureExec.executeWithSignature方法中,不仅会进行签名还会对响应进行验签。

		// 签名
        request.addHeader("Authorization", this.credentials.getSchema() + " " + this.credentials.getToken(request));
        // 执行请求
        CloseableHttpResponse response = this.mainExec.execute(route, request, context, execAware);
        // 获取响应
        StatusLine statusLine = response.getStatusLine();
        if (statusLine.getStatusCode() >= 200 && statusLine.getStatusCode() < 300) {
        	// 请求成功,对回调进行验签
            this.convertToRepeatableResponseEntity(response);
            if (!this.validator.validate(response)) {
                throw new HttpException("应答的微信支付签名验证失败");
            }
        } else {
            log.error("应答的状态码不为200-299。status code[{}]\trequest headers[{}]", statusLine.getStatusCode(), Arrays.toString(request.getAllHeaders()));
            if (this.isEntityEnclosing(request) && !this.isUploadHttpPost(request)) {
                HttpEntity entity = ((HttpEntityEnclosingRequest)request).getEntity();
                String body = EntityUtils.toString(entity);
                log.error("应答的状态码不为200-299。request body[{}]", body);
            }
        }
        return response;

验签调用的是WechatPay2Validator对象的validate方法,执行流程如下:

    public final boolean validate(CloseableHttpResponse response) throws IOException {
        try {
        	// 1. 校验响应参数
            this.validateParameters(response);
            // 2. 取出时间戳、随机字符串、响应体
            String message = this.buildMessage(response);
            // 3. 微信支付的平台证书序列号
            String serial = response.getFirstHeader("Wechatpay-Serial").getValue();
            // 4. 微信签名后的数据,应答和回调的签名验证使用的是 微信支付平台证书,不是商户API证书。
            String signature = response.getFirstHeader("Wechatpay-Signature").getValue();
            // 5. 调用证书管理器中的默认校验器进行验签
            if (!this.verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", serial, message, signature, response.getFirstHeader("Request-ID").getValue());
            } else {
                return true;
            }
        } catch (IllegalArgumentException var5) {
            log.warn(var5.getMessage());
            return false;
        }
    }

最终还是调用证书管理器中的默认校验器DefaultVerifier进行验签:

        public boolean verify(String serialNumber, byte[] message, String signature) {
            if (!serialNumber.isEmpty() && message.length != 0 && !signature.isEmpty()) {
            	// 微信平台证书序列号
                BigInteger serialNumber16Radix = new BigInteger(serialNumber, 16);
                // 从内存缓存中,获取该商户对应的微信平台证书(自动更新的,5分钟一次)
                ConcurrentHashMap<BigInteger, X509Certificate> merchantCertificates = (ConcurrentHashMap)CertificatesManager.this.certificates.get(this.merchantId);
                X509Certificate certificate = (X509Certificate)merchantCertificates.get(serialNumber16Radix);
                if (certificate == null) {
                    CertificatesManager.log.error("商户证书为空,serialNumber:{}", serialNumber);
                    return false;
                } else {
                    try {
                    	// 验签
                        Signature sign = Signature.getInstance("SHA256withRSA");
                        sign.initVerify(certificate);
                        sign.update(message);
                        return sign.verify(Base64.getDecoder().decode(signature));
                    } catch (NoSuchAlgorithmException var8) {
                        throw new RuntimeException("当前Java环境不支持SHA256withRSA", var8);
                    } catch (SignatureException var9) {
                        throw new RuntimeException("签名验证过程发生了错误", var9);
                    } catch (InvalidKeyException var10) {
                        throw new RuntimeException("无效的证书", var10);
                    }
                }
            } else {
                throw new IllegalArgumentException("serialNumber或message或signature为空");
            }
        }

总结

每次请求时,都会使用到商户证书、微信平台证书互相签名验签,确保支付安全性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/192692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode-136. 只出现一次的数字

目录题目分析哈希集位运算题目来源 https://leetcode.cn/problems/single-number/ 题目分析 题目有个条件可谓相当重要&#xff0c;即凡重复的元素最多重复一次&#xff08;原话&#xff1a;给定一个非空整数数组&#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个…

详解Mybatis-Plus中分页插件PaginationInterceptor, MybatisPlusInterceptor在SpringBoot中的使用

文章目录1. 描述1.1 MybatisPlusInterceptor1.2 InnerInterceptor2. 实现2.1 不带条件的分页查询2.2 带条件的分页查询2.3 简述Page类3. 注意事项1. 描述 1.1 MybatisPlusInterceptor 我们在开发的过程中&#xff0c;经常会遇到分页操作&#xff0c;其分为逻辑分页和物理分页…

Gif动态图片如何用静图制作?教你静图合成动图的方法

gif动图如何制作&#xff1f;相信对于gif动图大家都不陌生&#xff0c;在平时的聊天软件中、公众号文章中都可以看到。那么&#xff0c;要如何制作gif动图呢&#xff1f;下面&#xff0c;就给教大家两个在线gif制作&#xff08;https://www.gif.cn/&#xff09;的方法&#xff…

综合案例【商品管理系统-Java基础版】(附完整源码)

Java语言的一个超级简易的商品管理系统&#xff0c;适合初学者练手 源码包无法上传至资源&#xff08;blog已经超级完整啦&#xff09;如果还是需要完整源码src包可私分享 目录1 项目分析1.1 用户模块&#xff08;普通用户、管理员用户&#xff09;1.1.1前端系统&#xff08;普…

FluentReader为什么称之为高颜值的rss阅读器

Fluent Reader 这是一款 RSS 阅读器。对于很多年轻的网友来说&#xff0c;RSS 这个名词可能有点陌生。简单来说只要某个网站支持 RSS&#xff0c;你订阅了 RSS 后&#xff0c;一旦网站更新了内容&#xff0c;就会推送到你面前。这时候&#xff0c;我们就需要一款像 Fluent Rea…

全屋智能三国志

刚刚过去的春节假期&#xff0c;对我来说&#xff0c;是一个收集现实素材、感应社会变化的好机会。也确实发现了不少新趋势&#xff0c;一个结论是&#xff1a;智能家居正在酝酿新一轮的市场浪潮。以央视春晚作为切口&#xff0c;每年央视春晚的广告投放&#xff0c;都一定程度…

vscode 配置 plantuml

1、首先安装 plantuml 插件 2、安装 java 开发环境 如果是 mac 系统&#xff0c;直接执行 brew install java &#xff0c;然后按照下图执行下提示中的命令即可 如果是 windows 系统&#xff0c;需要去官网下载 java jdk&#xff0c;安装好之后添加运行路径到 path 中 https://…

基于 ShardingSphere 的分布式数据库负载均衡架构搭建实战

本文主要分为 3 部分&#xff0c;将依次介绍&#xff1a; 基于 ShardingSphere 的分布式数据库「负载均衡架构搭建」要点结合实际的「用户问题案例」&#xff0c;介绍引入「负载均衡」的影响介绍并展示 ShardingSphere 分布式数据库在云上的「一站式解决方案」 文章目录Shardi…

LED背光车载驱动IC 可支持48通道

特性电源电压范围-VDD:3.3V~5.5V-VLED:27V(max)48 个恒流输出通道- 通道恒流输出范围 0~50mA一 通道间电流输出偏差小于3%- 芯片间电流输出偏差小于 3%Low Knee Voltage:0.55V小于20mA0.6V 大于20mA最大支持4扫&#xff0c;内嵌行控制信号通道灰度实现-支持 PWM/PAMPWM驱动 -灰…

云帆文档管理系统版本更新说明:v4.6.0

一、新增及优化功能资料管理新增打包下载管理端增加资料查阅菜单&#xff0c;管理端和用户端用户有同样的查阅权限&#xff0c;方便其快速的查阅企业的资料信息前端下载文档格式添加下载水印&#xff0c;水印显示的是下载的用户和日期采购合同增加智能识别功能资料管理添加关键…

KPI考核系统实战之二:swagger框架

KPI考核系统实战之二&#xff1a;swagger框架一、asp.net core 3.1二、融合swagger1.Nuget安装Swashbuckle.AspNetCore2.Startup.cs 注册Swagger服务&#xff0c;使用swagger中间件一、asp.net core 3.1 使用Visual Studio 2022&#xff0c;搭建asp.net core weiapi开发平台&a…

煤矿皮带运行状态监测预警系统 yolov7

煤矿皮带运行状态监测预警系统通过yolov7网络模型深度学习技术&#xff0c;自动对传输皮带运行状态进行实时监测。当监测到皮带撕裂、跑偏、异物、堆煤等异常情况时&#xff0c;立即抓拍预警及时停止皮带同步回传违规信息到后台。介绍Yolo算法之前&#xff0c;首先先介绍一下滑…

PushKit/Callkit使用经验

前言&#xff1a;如果要求使用这两种库&#xff0c;请在查询资料并自己尝试后&#xff0c;多参考苹果官方的API文档&#xff1a;PushKit&#xff1a;https://developer.apple.com/documentation/pushkit?languageobjcCallKit&#xff1a;https://developer.apple.com/document…

人工智能与模式识别的意义(模式识别与图像处理课程作业)

人工智能与模式识别的意义(模式识别与图像处理课程作业一、 人工智能的意义二、 模式识别的意义2.1、文字识别2.2、语音识别2.3、指纹识别2.4、遥感2.5、医学诊断1、语音识别技术2、生物认证技术3、数字水印技术一、 人工智能的意义 人工智能的发送对于我们社会的各个方面都具有…

Word页面中四个直角

文章目录1、四个直角1&#xff09;代表页边距2&#xff09;页边距的设置3&#xff09;打开或关闭“裁剪标记”2、“裁剪标记”与图片1&#xff09;插入图片超过这个角能打印显示出来吗&#xff1f;3、“裁剪标记”与表格1、四个直角 1&#xff09;代表页边距 页面中的四个角代…

【LoRa网关以及LoRa自组网】以“有人物联网”为例

【LoRa网关以及LoRa自组网】以“有人物联网”为例0.参考资料1. LoRa 自组网协议的理解1.1【LoRa模块WH-L101-L-P-H10 】1.2【LoRa网关设置】1.3【节点、网关、服务器通讯】1.4【一些注意事项】1.5【专业名词】2.【LoRa点对点通讯 】LoRa网关可以实现多个LoRa节点的数据采集&…

咖啡商城|基于Springboot+Vue前后端分离咖啡商城系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

微软开源的 5 个 yyds 课程!

微软在 GitHub 开源了五大课程&#xff0c;面向计算机专业或者入门编程的同学。分别是 Web 开发课程、机器学习课程、物联网课程、数据分析课程、Bash 课程。01为初学者准备的 Web开发课程这个 65k Star 的 Web 开发课程由 Microsoft 的 Azure Cloud Advocates 出品&#xff0…

prometheus登录认证

目标 登录Prometheus的9090端口页面的时候&#xff0c;需要输入用户名和密码&#xff0c;才能进入Prometheus页面。 设置密码 Prometheus配置密码不能是明文&#xff0c;必须经过bcrypt程序对密码进行Hash处理。 vim gen-pass.py 内容如下&#xff1a; import getpass import…

Node,docker 中安装node.js

1.启动docker服务 首先启动docker服务:systemctl start docker 2.获取node最新镜像 启动完成之后拉取node最新镜像&#xff1a;docker pull node 然后开始等待&#xff0c;最后拉取完成会有相应的输出信息。 我们再通过命令确认下node是否拉取成功&#xff1a;docker image…