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


文章目录

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


前言

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

签名生成

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

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

CloseableHttpResponse response = httpClient.execute(httpPost);

httpPost包含了请求路径、请求头、请求体等内容。

Java 支付宝签名 和验签 支付验证签名_Java 支付宝签名 和验签


经过HttpClient自身框架的一些处理,进入到请求执行器RetryExec执行方法中,

Java 支付宝签名 和验签 支付验证签名_Java 支付宝签名 和验签_02

接着调用请求执行器(微信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头部中包括应答签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

请求成功后,响应回来的信息如下:

Java 支付宝签名 和验签 支付验证签名_微信_03


在执行并签名的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为空");
            }
        }

总结

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