目录
一、签名分析
1.1 流程分析
1.构造签名串
2.计算签名值
3.设置请求头
二、源码级别分析
二、获取平台证书分析
三、验签分析
3.1 验签使用场景:
3.2 验证流程:
1.获取微信平台证书列表
2.检查平台证书序列号
3.2 验签源码分析
1.分析
2.总结:
在商户系统(自开发)向微信支付平台系统发送请求,和接收结果的过程中,都是有生成签名和校验签名的过程,只是这些操作都被 SDK 的 生成CloseableHttpClient 之前已经处理过了。
一、签名分析
1.1 流程分析
签名的生成分为3步骤,在SDK中也有代码的体现。
1.构造签名串
签名串一共有五行,每一行为一个参数。行尾以 \n(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。
HTTP请求方法\n GET\n
URL\n /v3/certificates\n
请求时间戳\n 1554208460\n
请求随机串\n 593BEC0C930BF1AFEB40B4A08C8FB242\n
请求报文主体\n {ifn:xxxx} \n
2.计算签名值
绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
通过商户私钥使用 SHA256 withRSA 算法加密 ,在进行 BASE64 处理。
3.设置请求头
微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成。
认证类型 :这是使用什么类型进行加密处理
Authorization: 认证类型 签名信息
具体组成为:
1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
2.签名信息
发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
商户API证书序列号serial_no,用于声明所使用的证书
请求随机串nonce_str
时间戳timestamp
签名值signature
注:以上五项签名信息,无顺序要求。
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"
二、源码级别分析
1.通过log日志的打印得出 在生成 ScheduledUpdateCertificatesVerifier 校验器的时候已经 做好了签名的生成工作。
- 生成签名串
- 计算签名值
- 设置HTTP请求信息 TOKEN参数的封装
2.通过日志分析得出处理类是 WechatPay2Credentials,进行了签名的处理。
public class WechatPay2Credentials implements Credentials {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class);
// 私用符号
protected static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
// 安全随机数生成器
protected static final SecureRandom RANDOM = new SecureRandom();
// 商户编号
protected final String merchantId;
// 签名生成器
protected final Signer signer;
public WechatPay2Credentials(String merchantId, Signer signer) {
this.merchantId = merchantId;
this.signer = signer;
}
public String getMerchantId() {
return merchantId;
}
// 获取当前时间戳
protected long generateTimestamp() {
return System.currentTimeMillis() / 1000;
}
// 生成字符串
protected String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
// 获取认证类型
@Override
public final String getSchema() {
return "WECHATPAY2-SHA256-RSA2048";
}
/**
* 生成签名串+计算签名值+设置HTTP请求头
*
**/
@Override
public final String getToken(HttpRequestWrapper request) throws IOException {
// 生成随机字符串
String nonceStr = generateNonceStr();
// 生成当前时间戳
long timestamp = generateTimestamp();
// 构造签名串
String message = buildMessage(nonceStr, timestamp, request);
log.debug("authorization message=[{}]", message);
// 使用 PrivateKeySigner 进行签名算法处理 ,通过 商户私钥 + SHA256withRSA 的摘要计算 对签名数据进行加密处理
Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
String token = "mchid=\"" + getMerchantId() + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + signature.certificateSerialNumber + "\","
+ "signature=\"" + signature.sign + "\"";
log.debug("authorization token=[{}]", token);
return token;
}
/**
* 构造签名串方法
* @Param nonce 随机字符串
* @Param timestamp 当前时间戳
* @Param request 请求对象
*
**/
protected String buildMessage(String nonce, long timestamp, HttpRequestWrapper request) throws IOException {
// 1.获取当前的请求路径对象,
URI uri = request.getURI();
// 2.获取到当前请求uri 路径
String canonicalUrl = uri.getRawPath(); // " /v3/certificates "
// 3.判断是否有请求参数如果有则进行拼接上
if (uri.getQuery() != null) {
canonicalUrl += "?" + uri.getRawQuery();
}
// 4.签名主体
String body = "";
// 判断是否微信上传文件请求 请求方法为GET时,报文主体为空。
// 当请求方法为POST或PUT时,请使用真实发送的JSON报文。
// 图片上传API,请使用meta对应的JSON报文。
// PATCH,POST,PUT
if (request.getOriginal() instanceof WechatPayUploadHttpPost) {
body = ((WechatPayUploadHttpPost) request.getOriginal()).getMeta();
//
} else if (request instanceof HttpEntityEnclosingRequest) {
body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity(), StandardCharsets.UTF_8);
}
// 5.构建签名串 GET请求 body 是空的
return request.getRequestLine().getMethod() + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
}
/**
* @author xy-peng
*/
public class PrivateKeySigner implements Signer {
// 商户证书序列号
protected final String certificateSerialNumber;
// 商户私钥
protected final PrivateKey privateKey;
public PrivateKeySigner(String serialNumber, PrivateKey privateKey) {
this.certificateSerialNumber = serialNumber;
this.privateKey = privateKey;
}
// 对签名串进行摘要处理加密处理
@Override
public SignatureResult sign(byte[] message) {
try {
// 创建SHA256withRSA加密方式的签名生成器实例对焦
Signature sign = Signature.getInstance("SHA256withRSA");
// 初始化签名生成器,将商户私钥配置
sign.initSign(privateKey);
// 设置要加密的数据
sign.update(message);
// 签名生成方法,并将结果进行 Base64的编码处理,并他封装签名结果对象
return new SignatureResult(Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名计算失败", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的私钥", e);
}
}
}
4.在 SignatureExec 的 executeWithSignature 方法中设置HTTP请求信息,并执行请求方法,获取请求结果,并进行结果的验签操作。
签名生成流程总结
- 1.在获取 ScheduledUpdateCertificatesVerifier(签名校验器) 的过程中 调用 了 WechatPay2Credentials的 getToken 方法生成签名,该方法执行了生成: 1.签名串 2.将签名串进行加密处理
- 2.getToken中调用了buildMessage 来生成签名串数据。
- 3.buildMessage 方法中生成
- 4.在getToken方法中调用了PrivateKeySigner 的 sign(), 这个方法底层调用 java. security 包下 的 SignatureSpi 的相关签名处理方法。
- 5.执行完签名的后,SignatureExecexecuteWithSignature 方法中执行,设置HTTP请求头和执行请求方法来,并解析请求参数验证结果信息。
二、获取平台证书分析
核心的类是 ScheduledUpdateCertificatesVerifier 是在原有CertificatesVerifier基础上,增加定时更新证书功能(默认1小时)
/**
* 初始化平台证书管理器实例,在使用前需先调用该方法
*
* @param credentials 认证器
* @param apiV3Key APIv3密钥
* @param minutesInterval 定时更新间隔时间
*/
public synchronized void init(Credentials credentials, byte[] apiV3Key, long minutesInterval) {
if (credentials == null || apiV3Key.length == 0 || minutesInterval == 0) {
throw new IllegalArgumentException("credentials或apiV3Key或minutesInterval为空");
}
if (this.credentials == null || this.apiV3Key.length == 0 || this.executor == null
|| this.certificates == null) {
this.credentials = credentials;
this.apiV3Key = apiV3Key;
this.executor = new SafeSingleScheduleExecutor();// 创建线程池
this.certificates = new ConcurrentHashMap<>(); // 证书存放的Map
// 初始化证书
initCertificates();
// 启动定时更新证书任务
Runnable runnable = () -> {
try {
Thread.currentThread().setName(SCHEDULE_UPDATE_CERT_THREAD_NAME);
log.info("Begin update Certificate.Date:{}", Instant.now());
updateCertificates(); // 更新证书的方法
log.info("Finish update Certificate.Date:{}", Instant.now());
} catch (Throwable t) {
log.error("Update Certificate failed", t);
}
};
// 执行线程任务
executor.scheduleAtFixedRate(runnable, 0, minutesInterval, TimeUnit.MINUTES);
}
}
// 更新证书的方法
private void updateCertificates() {
Verifier verifier = null;
if (!certificates.isEmpty()) {
verifier = new CertificatesVerifier(certificates);
}
// 调用下载证书的方法
downloadAndUpdateCert(verifier);
}
// 初始化证书方法
private void initCertificates() {
downloadAndUpdateCert(null);
}
// 线程安全的证书下载方法
private synchronized void downloadAndUpdateCert(Verifier verifier) {
// 创建出httpClient对象
try (CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withCredentials(credentials)
.withValidator(verifier == null ? (response) -> true
: new WechatPay2Validator(verifier))
.build()) {
// 发送GET请求设置请求通信息
HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH);
httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString());
// 发送请求
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
// 获取结果code
int statusCode = response.getStatusLine().getStatusCode();
// 获取结果数据
String body = EntityUtils.toString(response.getEntity());
// 如果结果的 200
if (statusCode == SC_OK) {
// 将证书文件解码后存放到map中
Map<BigInteger, X509Certificate> newCertList = CertSerializeUtil.deserializeToCerts(apiV3Key, body);
if (newCertList.isEmpty()) {
log.warn("Cert list is empty");
return;
}
// 清除所有的证书
certificates.clear();
// 重新添加证书
certificates.putAll(newCertList);
} else {
log.error("Auto update cert failed, statusCode = {}, body = {}", statusCode, body);
}
}
} catch (IOException | GeneralSecurityException e) {
log.error("Download Certificate failed", e);
}
}
总结:
证书每隔60分钟刷新证书,两个核心东西一个是定时器一个是存储身份信息对象(Credentials),证书更新是调用证书下载的方法,证书信息被放到响应体中被加密处理过,用的是对称加密的密钥,获取到证书数据后进行解密,然后存放到缓存Map中,每次重新获取证书都会清空当前map。
三、验签分析
3.1 验签使用场景:
需要验证签名的时间点分为两处:
- 1.商户(我们系统)主要向微信支付系统发送,微信支付系统向我们响应结果时。
- 2.微信系统回调商户系统,需要对数据验证操作,防止伪造数据
注意:应答签名验证是可做可不做的,回调必须要验证签名。
如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。
3.2 验证流程:
1.获取微信平台证书列表
- 接口:https://api.mch.weixin.qq.com/v3/certificates
- 在上方验签请求就是在获取微信平台证书。
- 微信支付的公钥只能从从微信平台证书中获取。
- 微信支付的公钥只能从从微信平台证书中获取。
- 平台证书是一个列表,因为证书是有一个有效期的,如果证书过期,新的证书还没有生成,那么就会有一个证书的空档期,所有微信会把提前24小时的新证书加入到平台证书列表中,所以平台证书会存在两个一个是将要过期的,一个即将启用的。
- 返回结果会提供微信支付平台证书的序列号,serial_no 进行证书区分。
200: OK
{
"data": [
{
"serial_no": "5157F09EFDC096DE15EBE81A47057A7232F1B8E1",
"effective_time ": "2018-06-08T10:34:56+08:00",
"expire_time ": "2018-12-08T10:34:56+08:00",
"encrypt_certificate": {
"algorithm": "AEAD_AES_256_GCM",
"nonce": "61f9c719728a",
"associated_data": "certificate",
"ciphertext": "sRvt… "
}
},
{
"serial_no": "50062CE505775F070CAB06E697F1BBD1AD4F4D87",
"effective_time ": "2018-12-07T10:34:56+08:00",
"expire_time ": "2020-12-07T10:34:56+08:00",
"encrypt_certificate": {
"algorithm": "AEAD_AES_256_GCM",
"nonce": "35f9c719727b",
"associated_data": "certificate",
"ciphertext": "aBvt… "
}
}
]
}
2.检查平台证书序列号
微信支付的平台证书序列号位于HTTP头Wechatpay-Serial。验证签名前,请商户先检查序列号是否跟商户当前所持有的 微信支付平台证书的序列号一致。如果不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将无法成功验证签名。
3.构造签名串
- 首先,商户先从应答中获取以下信息:
- HTTP头Wechatpay-Timestamp 中的应答时间戳
- HTTP头Wechatpay-Nonce 中的应答随机串。
- 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。
- 然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。
应答时间戳\n
应答随机串\n
应答报文主体\n
- 如某个应答的HTTP报文为(省略了ciphertext的具体内容):
HTTP/1.1 200 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c // 应答随机串
// 应答签名
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980 // 应答时间戳
Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
Cache-Control: no-cache, must-revalidate
// 应答报文主体
{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
- 则验签名串为
1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
4.获取应答签名
- 微信支付的应答签名通过HTTP头Wechatpay-Signature传递。(注意,示例因为排版可能存在换行,实际数据应在一行)
- 对 Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
5.验证签名
- 很多编程语言的签名验证函数支持对验签名串和签名 进行签名验证。强烈建议商户调用该类函数,使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。
- (1) 首先,从微信支付平台证书导出微信支付平台公钥
- (2) 然后,把签名base64解码。
- (3) 最后验证签名,验证签名的流程是:
- 1.拿到刚刚生成签名,使用 SHA256 算法+ 微信公钥 进行摘要计算。
- 2.将得到的结果和 应到签名进行比较判断是否一致,如果一致则为正常,否则失败。
3.2 验签源码分析
1.分析
通过源码分析得出真正的验签的类是 WechatPay2Validator ,这个类中有一个方法 validate 进行执行验证操作。
package com.wechat.pay.contrib.apache.httpclient.auth;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.REQUEST_ID;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_NONCE;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP;
import com.wechat.pay.contrib.apache.httpclient.Validator;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author xy-peng
*/
public class WechatPay2Validator implements Validator {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
// 验证器
protected final Verifier verifier;
public WechatPay2Validator(Verifier verifier) {
this.verifier = verifier;
}
// 参数异常处理方法
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
// 验证失败处理异常
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
// 核心方法验证方法
@Override
public final boolean validate(CloseableHttpResponse response) throws IOException {
try {
// 验证响应的结果参数是
validateParameters(response);
// 生成签名
String message = buildMessage(response);
// 获取微信平台证书序列号
String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue();
// 获取当前应答签名
String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();
// 验签比较方法,
if (!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());
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
// 校验参数方法
protected final void validateParameters(CloseableHttpResponse response) {
// 获取第一个请求为 Request-ID
Header firstHeader = response.getFirstHeader(REQUEST_ID);
// 如果请求头为null则验证失败
if (firstHeader == null) {
throw parameterError("empty " + REQUEST_ID);
}
// 获取请求id
String requestId = firstHeader.getValue();
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
// 组装一个 响应的参数名称集合 有 证书序列号、应答签名、时间戳、随机字符串
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
Header header = null;
// 遍历所需要的请求的参数名取从应答对象中拿如果没有则报错
for (String headerName : headers) {
// 拿去所需要的应答对象数据
header = response.getFirstHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
// 因为最后的值是时间戳,所以这里直接获取到时间戳了
String timestampStr = header.getValue();
try {
// 将时间戳进行转换为时间
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 比较应答时间和当前时间超过默认5分钟, 拒绝过期应答
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);
}
}
// 验签名串
protected final String buildMessage(CloseableHttpResponse response) throws IOException {
String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
String body = getResponseBody(response);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
// 获取报文主体结果
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
- 真正验签会走到 CertificatesVerifier 的 verify 方法
- certificates.get(val) 是根据 微信平台证书序列号从map中获取证书信息
- 获取到验证器会调用 verify 进行组装 参数 最终调用 Signature 的 verify 进行验证
protected boolean verify(X509Certificate certificate, byte[] message, String signature) {
try {
// 创建签名器,并指定签名算法
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(certificate); // 设置签名器使用的证书公钥
sign.update(message);// 要验证的数据
// 验证传入的签名数据 也就是 update 和 verify 数据进行比较
return sign.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名验证过程发生了错误", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的证书", e);
}
}
- 从图中可以看出 传来参数 var1 就是应答的签名,首先对应答对象进行了 rsa 公钥的解密得到 var3
- var2 是对生成的签名串进行摘要计算
- 最后将应答签名和生成的签名进行equal比较。
// sigBytes 应答的签名结果
@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
if (publicKey == null) {
throw new SignatureException("Missing public key");
}
try {
if (sigBytes.length != RSACore.getByteLength(publicKey)) {
throw new SignatureException("Signature length not correct: got " +
sigBytes.length + " but was expecting " +
RSACore.getByteLength(publicKey));
}
// 获取生成的签名串的摘要结果
byte[] digest = getDigestValue();
// 将应答数据进行rsa使用微信公钥解密
byte[] decrypted = RSACore.rsa(sigBytes, publicKey);
// 将处理的值进行拆包处理
byte[] unpadded = padding.unpad(decrypted);
// 使用digestOID 进行解密处理 得出 摘要的数据
byte[] decodedDigest = decodeSignature(digestOID, unpadded);
// 将摘要的值和应答的值进行equal比较
return MessageDigest.isEqual(digest, decodedDigest);
} catch (javax.crypto.BadPaddingException e) {
// occurs if the app has used the wrong RSA public key
// or if sigBytes is invalid
// return false rather than propagating the exception for
// compatibility/ease of use
return false;
} catch (IOException e) {
throw new SignatureException("Signature encoding error", e);
} finally {
resetDigest();
}
}
2.总结:
正常的签名流程是 是对数据先进行摘要,在进行加密处理,然后在进行Base64的处理,验签也是反过来,最后一部就是验证签名
1.获取应答对象的请求头数据
2.将数据生成签名串
3.获取应答签名串
4.将生成的签名串进行摘要计算,生成摘要串。
5.使用rsa算法对应答签名进行解密得出摘要结果。
6.将生成的摘要串和解密的摘要串进行比较 。
检验的话他会先将用公钥对解码后(base64)的数据进行解密获取到加密的数据,在后用 使用rsa算法+微信公钥进行解码得出 sha256 对之前获取到微信的签名串数据进行摘要运算,在将在将两个摘要数据进行比较是否相同从而验证是否合法性。因为摘要运算,相同的数据得出的结果都是相同的。