对接微信支付APIv3版本签名验签学习
学习整理了一下对接微信支付接口,不是使用SDK版本的,纯java开发。
微信官方参考网站:
https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-generation.html
按照微信官方接口说明进行开发
一. 前提:申请商户号和证书
商户需要拥有一个微信支付商户号,并通过超级管理员账号登录商户平台,获取商户API证书。 商户API证书的压缩包中包含了签名必需的私钥和商户证书。
二. 签名
微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。Authorization由认证类型和签名信息两个部分组成。
具体组成为:
- 认证类型,目前为WECHATPAY2-SHA256-RSA2048
- 签名信息
- 发起请求的商户(包括直连商户、服务商或渠道商)的商户号mchid
- 商户API证书序列号serial_no,用于声明所使用的证书
tip:(商户P12证书的序列号)
- 请求随机串nonce_str
tip:(随机数,UUID或者雪花ID都可以)
- 时间戳timestamp
tip:(System.currentTimeMillis() / 1000)
- 签名值signature
tip:(SHA256签名)
提示
注意:以上五项签名信息,无顺序要求。
1、针对签名部分
下面方法是构造签名串和使用SHA256进行签名
/**
* @param requestType HTTP请求方法类型 GET\POST\PUT\DELETE
* @param url URL
* @param timestamp 请求时间戳
* @param nonceStr 请求随机串
* @param body 请求报文主体 部分get请求是没有body请求体的
*/
public static String getSignContent(String requestType, String url, long timestamp, String nonceStr, String body) {
StringBuilder signContent = new StringBuilder();
signContent.append(requestType).append("\n");
signContent.append(url.substring(url.indexOf("/v3"))).append("\n");
signContent.append(timestamp).append("\n");
signContent.append(nonceStr).append("\n");
if (StringUtils.isNotEmpty(body)) {
signContent.append(body).append("\n");
} else {
signContent.append("\n");
}
return signContent.toString();
}
/**
* 签名
* @param message -> getSignContent方法返回的内容
* @param filePath -> 商户P12证书的路径
* @param pwd -> 商户P12证书的密码
* 签名串最后是返回Base64的
*/
public static String sha256sign(String message, String filePath, String pwd) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(getPrivateKey(filePath, pwd));
signature.update(message.getBytes(StringUtils.DEFAULT_CHARSET));
return new String(Base64.encode(signature.sign()), StringUtils.DEFAULT_CHARSET);
}
2、Authorization: 认证类型 签名信息
按照上面说的要求,把5部分的内容进行拼接,设置请求头中 Authorization 部分
StringBuilder authorization = new StringBuilder();
authorization.append("WECHATPAY2-SHA256-RSA2048 ");
authorization.append("mchid=\"" + 商户号 + "\",");
authorization.append("nonce_str=\"" + nonceStr + "\",");
authorization.append("timestamp=\"" + timestamp + "\",");
authorization.append("serial_no=\"" + getSerialno(keyPath, pwd) + "\",");
authorization.append("signature=\"" + signature + "\"");
3、如果是post、put操作,需要设置请求体
//上面签名方法中的body内容放到httpEntity中
HttpEntity httpEntity = = new StringEntity(body, "UTF-8");
4、发送Post请求 PUT也一样(new HttpPut)
/**
* http post请求
*
* @param url 请求地址
* @param httpEntity 请求内容
* @param authorization 授权信息
* @param headerList 请求头集合
* @param pwd P12证书密码
* @param filePath P12证书路径
*/
public static JSONObject httpPost(String url, HttpEntity httpEntity, String authorization, String pwd, String filePath) throws Exception {
InputStream inputStream = null;
CloseableHttpClient httpClient = createSSLClientDefault();
JSONObject jsonObj = new JSONObject();
try {
HttpPost httpPost = new HttpPost(url);
// 请求头
httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
httpPost.addHeader("Authorization", authorization);
httpPost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)");
httpPost.addHeader("Accept", "application/json");
// 请求体
if (httpEntity != null) {
httpPost.setEntity(httpEntity);
}
// 请求配置
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(15000)
.setConnectTimeout(15000)
.setConnectionRequestTimeout(15000)
.build();
httpPost.setConfig(requestConfig);
HttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
// 获取响应码
jsonObj.put("statusCode", statusCode);
Header[] headers = response.getAllHeaders();
HttpEntity entity = response.getEntity();
if (null != entity) {
inputStream = entity.getContent();
String responseJson = new String(IoUtils.read(inputStream, 1024), "utf-8");
// 验签
if (verify(headers, responseJson, merchantId, pwd, filePath)) {
// 获取响应内容
jsonObj.put("detail", responseJson);
} else {
System.out.println("验签失败");
System.out.println(responseJson);
}
}
} finally {
IOUtils.closeQuietly(inputStream);
httpClient.close();
}
return jsonObj;
}
5、发送Get类请求 Delete也一样(new HttpDelete)
public static JSONObject httpGet(String url, String authorization, String pwd, String filePath) throws Exception {
InputStream inputStream = null;
CloseableHttpClient httpClient = createSSLClientDefault();
JSONObject jsonObj = new JSONObject();
try {
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Content-Type", "application/json; charset=utf-8");
httpGet.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)");
httpGet.addHeader("Accept", "application/json");
httpGet.addHeader("Authorization", authorization);
RequestConfig requestConfig;
requestConfig = RequestConfig.custom()
.setSocketTimeout(15000)
.setConnectTimeout(15000)
.setConnectionRequestTimeout(15000)
.build();
httpGet.setConfig(requestConfig);
HttpResponse response = httpClient.execute(httpGet);
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();
jsonObj.put("statusCode", statusCode);
Header[] headers = response.getAllHeaders();
HttpEntity entity = response.getEntity();
if (null != entity) {
inputStream = entity.getContent();
String responseJson = new String(IoUtils.read(inputStream, 1024), StringUtils.DEFAULT_CHARSET);
if (verify(headers, responseJson, merchantId, pwd, filePath)) {
jsonObj.put("detail", responseJson);
} else {
System.out.println("验签失败");
System.out.println(responseJson);
}
}
} finally {
IOUtils.closeQuietly(inputStream);
httpClient.close();
}
return jsonObj;
}
三.验签 使用平台证书,而不是商户证书
验签使用微信支付平台证书中的公钥验签。目前,微信支付平台证书仅提供 API 下载
下载平台证书对应的微信API地址:
https://pay.weixin.qq.com/docs/merchant/apis/platform-certificate/api-v3-get-certificates/get.html
在验证签名前,微信要求先检查 HTTP 头 Wechatpay-Serial
的内容是否跟商户当前所持有的微信支付平台证书的序列号一致。若不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将验证失败。
//忽略getter、setter方法
public class EncryptCert {
/**
* 证书序列号
*/
private String serialNo;
/**
* 证书生效时间
*/
private String effectiveTime;
/**
* 证书失效时间
*/
private String expireTime;
/**
* 证书使用的算法
*/
private String algorithm;
/**
* 证书加密使用的随机串初始化向量
*/
private String nonce;
/**
* 附加数据包(可能为空)
*/
private String associatedData;
/**
* Base64编码后的密文
*/
private String ciphertext;
/**
*
*/
private PublicKey publicKey;
}
所以需要调用下载证书接口,获取最新的证书,比对序列号,然后进行验签。
/**
* 验签
*
* @param headers 响应的请求头数组
* @param message 响应
* @param merchantId 商户号
* @param pwd P12证书密码
* @param filePath P12证书路径
*/
pubilc static boolean verify(Header[] headers, String message, String merchantId, String pwd, String filePath) throws Exception {
String timestamp = null;
String nonce = null;
String signature = null;
String serial = null;
for (Header header : headers) {
String name = header.getName();
String value = header.getValue();
timestamp = name.equals("Wechatpay-Timestamp") ? value : timestamp;
nonce = name.equals("Wechatpay-Nonce") ? value : nonce;
signature = name.equals("Wechatpay-Signature") ? value : signature;
serial = name.equals("Wechatpay-Serial") ? value : serial;
}
// 下载并比较序列号进行验签
EncryptCert encryptCert = downloadCert(merchantId, filePath);
// 当匹配到对应的服务商证书后,说明可以验签了
if (serial.equalsIgnoreCase(encryptCert.getSerialNo())) {
String signStr = timestamp + "\n" + nonce + "\n" + message + "\n";
Signature instance = Signature.getInstance("SHA256withRSA");
instance.initVerify(encryptCert.getPublicKey());
instance.update(signStr.getBytes(StandardCharsets.UTF_8));
boolean verify = instance.verify(Base64.decode(signature));
if (verify) {
System.out.println("验签成功!");
} else {
System.out.println("验签失败!!!");
}
return verify;
}
System.out.println("验签失败!!!");
return false;
}
/**
* 获取微信平台证书
*
* @return
* @throws Exception
*/
public static EncryptCert downloadCert(String merchantId, String filePath) throws CodeException {
try {
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = UUID.randomUUID().toString();
String signature = sha256sign(getSignContent("GET", downloadCertUrl, timestamp, nonceStr, ""), filePath, distributorID);
String authorization = getAuthorization(timestamp, nonceStr, signature, merchantId, filePath, merchantId);
// 通过Get请求获取证书内容,这块和Gget代码一致,只是没有验签方法
JSONObject respJson = httpGetNotVer(downloadCertUrl, authorization, distributorID, filePath);
System.out.println(respJson.toJSONString());
if (respJson.isEmpty()) {
throw new RunTimeException("公钥证书下载失败");
}
if (!"200".equals(StringUtils.trim(respJson.getString("statusCode")))) {
throw new RunTimeException("公钥证书下载失败");
}
// 解析证书信息
JSONObject detailJson = JSONObject.parseObject(StringUtils.trim(respJson.getString("detail")));
JSONArray dataArray = JSONObject.parseArray(StringUtils.trim(detailJson.getString("data")));
if (dataArray.isEmpty()) {
throw new RunTimeException("公钥证书下载失败");
}
// 解析商户的所有证书
List<EncryptCert> certList = getCertList(dataArray);
// 获取商户最新的证书
EncryptCert cert = getLastCert(certList);
// 解密内容
String decrypt = decryptToString(cert.getAssociatedData().getBytes(),
cert.getNonce().getBytes(), cert.getCiphertext());
System.out.println("ciphertext解密内容:");
System.out.println(decrypt);
InputStream in = new ByteArrayInputStream(decrypt.getBytes("UTF-8"));
cert.setPublicKey(CertificateFactory.getInstance("X.509").generateCertificate(in).getPublicKey());
return cert;
} catch (Exception e) {
e.printStackTrace();
throw new RunTimeException("公钥证书下载失败");
}
}
/**
* 解析渠道商下所有证书
* @param dataArray 响应数据集
*/
private static List<EncryptCert> getCertList(JSONArray dataArray) {
int size = dataArray.size();
List<EncryptCert> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
JSONObject certJson = JSONObject.parseObject(dataArray.get(i).toString());
EncryptCert cert = new EncryptCert();
cert.setSerialNo(certJson.getString("serial_no"));
cert.setEffectiveTime(certJson.getString("effective_time"));
cert.setExpireTime(certJson.getString("expire_time"));
JSONObject encryptCertJson = JSONObject.parseObject(certJson.getString("encrypt_certificate"));
cert.setAlgorithm(encryptCertJson.getString("algorithm"));
cert.setNonce(encryptCertJson.getString("nonce"));
cert.setAssociatedData(encryptCertJson.getString("associated_data"));
cert.setCiphertext(encryptCertJson.getString("ciphertext"));
list.add(cert);
}
return list;
}
/**
* 获取最新的证书
* @param certList 证书集
*/
private static EncryptCert getLastCert(List<EncryptCert> certList) {
int size = certList.size();
if (size == 1) {
return certList.get(0);
}
int index = 0;
String effectiveTime = certList.get(0).getEffectiveTime();
for (int i = 1; i < size; i++) {
// 取值规则:生效时间最大的
if (effectiveTime.compareTo(certList.get(i).getEffectiveTime()) < 0) {
effectiveTime = certList.get(i).getEffectiveTime();
index = i;
}
}
return certList.get(index);
}
/**
* 微信对密文进行解密
* @param associatedData
* @param nonce
* @param ciphertext
*/
public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws Exception {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(BankAT511B.apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.decode(ciphertext)), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
}
四.总结
以上就是对微信支付接入后,签名、验签、解密的所有内容,demo就不放出来了,上面所表述的基本就是核心部分,
总体来看:
1、获取商户号
2、获取商户对应的私钥
3、签名,有格式要求,请求头中设置
4、涉及加密内容,需要借助平台公钥进行加密
5、发送对应的POST/GET/PUT/DELETE 请求
6、下载平台证书进行验签(这个公钥也是加解密用的)
7、解密数据
纯手打内容,如果涉及部分不正确,或者代码粘贴下来有问题,自行阅读处理一下就行,请大家理解哈~