有道无术,术尚可求,有术无道,止于术。
文章目录
- 前言
- 签名生成
- 签名验证
- 总结
前言
在上篇文档中,我们简单实现了对接微信支付的几个接口。了解到wechatpay-apache-httpclient
框架自动实现了签名和验签,接下来跟踪下源码,了解下具体流程~
签名生成
微信支付API v3
要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3
将会拒绝处理请求,并返回401 Unauthorized
。
在HttpClient
执行下单请求时打入断点。
CloseableHttpResponse response = httpClient.execute(httpPost);
httpPost
包含了请求路径、请求头、请求体等内容。
经过HttpClient
自身框架的一些处理,进入到请求执行器RetryExec
执行方法中,
接着调用请求执行器(微信SDK
中SignatureExec
类)执行,会判断请求的主机地址是否以.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-RSA2048
,getToken(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为空");
}
}
总结
每次请求时,都会使用到商户证书、微信平台证书互相签名验签,确保支付安全性。