1. 私钥和证书
1.1 商户API证书
1.1.1 功能介绍
API证书,是指由商户申请的,用来证实商户身份的证书。API证书由证书授权机构Certificate Authority(简称CA)颁发。证书中包含商户的商户号、公司名称、公钥等信息。
1.1.2 作用
签名生成
用来对请求url和body啥的签名,就是加密,微信收到请求,用证书公钥解密
商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem 中
加密用的就是,apiclient_key.pem文件,生成证书文件时,另外两个没用到
使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值
1.2 微信支付平台证书
1.2.1 功能介绍
微信支付平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。
1.2.2 作用
- 如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名,这时候就需要微信支付平台证书(给的是公钥)进行验签,保证是微信端的
- 回调信息和所有请求回来的信息都需要验签就需要,微信支付平台证书,他们那边用这个私钥对报文可能是body啥的加签,我们用公钥验签,保证安全
- 敏感信息加解密:上传某些重要信息时也需要用到这个微信支付平台证书公钥加密
1.2.3获取
获取平台证书
需要请求接口获取
1.2.4时效性
由于证书存在有效期的限制,微信支付会不定期地更换平台证书以确保交易安全。
估计12小时有效,所以要定期请求更新
1.3 apiv3密钥
为了保证安全性,微信支付在 回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。API v3密钥是加密时使用的对称密钥。
回调的信息用AES解密(密钥为APIV3值),得到的信息就是回调的明文内容
1.4 常见问题
第一次下载证书
对于微信支付平台的应答,需要使用平台证书来进行验签;但平台证书只能通过 获取平台证书接口 下载,所以当第一次去获取证书时,会出现个“死循环”。
为解决这个“死循环”,可以临时跳过验签,来获得证书。也就是说可以不提供微信支付证书参数(-c 参数)来下载,在下载得到证书后,工具会使用证书对报文的签名进行验证,如果通过则说明证书正确。
如何保证证书正确
工具已经从以下方面去保证了:
HTTPS:证书下载请求使用了 HTTPS
AES 加密:微信支付对证书信息进行了 AES-256-GCM 加密,所以工具得到应答后,会使用对称密钥来解密证书(这里需要用户传入对称密钥,出于对对称密钥安全的考虑,后续版本将可直接保存未解密的证书,由用户进行解密)
报文验签:微信支付会在应答的 HTTP 头包含签名,工具会通过解密得到的证书,来验证报文的签名,以此确认证书正确
1.5 官方SDK使用
wechatpay-apache-httpclient
实现了请求签名的生成和应答签名的验证,下载证书做了封装,没其他功能
httpClient记得设置超时时间,不然是默认的无超时,到时候接口爆炸就GG
在封装一个通用的工具类,获取HttpClient,实例化一次就够了,但是SDK并没有提供是否已经实例化过,所以只能取巧,从证书管理器中获取verifier是否异常来判断是否有实例化过
警告:这种写法有bug,当apiv3或者其他实例化HttpClient的参数改变时,需要重启应用才能生效,当然可以搞成配置修改时,重新实例化HttpClient
@Component
public class WxApiV3Utils {
public static final String REDIS_KEY_RCHG_PARTNER_PAY_MRCH_CFG = "rchg_partner_pay_mrch_cfg";
private static final Logger logger = LoggerFactory.getLogger(WxApiV3Utils.class);
private static final Map<String, CloseableHttpClient> MERCHANT_ID_HTTP_CLIENT_MAP = new ConcurrentHashMap<>();
/**
* 证书管理器实例
*/
private final CertificatesManager certificatesManager = CertificatesManager.getInstance();
@Autowired
private RedisUtils redisUtils;
public CloseableHttpClient getHttpClient(String merchantId) throws Exception {
// 获取ApiV3配置
WxPayMrchCfg wxPayMrchCfg = getApiV3Cfg(merchantId);
String merchantSerialNumber = wxPayMrchCfg.getCertSerialNo();
String apiV3Key = wxPayMrchCfg.getPartnerKey();
String certPrivateKeyPath = wxPayMrchCfg.getCertPrivateKeyPath();
// 加载商户私钥
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(certPrivateKeyPath));
// 获取验签器
Verifier verifier = getVerifier(merchantId, merchantSerialNumber, apiV3Key, merchantPrivateKey);
Optional<CloseableHttpClient> optional = getHttpClient(merchantId, merchantSerialNumber, merchantPrivateKey,
verifier);
if (!optional.isPresent()) {
throw new RuntimeException("httpClient为空");
}
return optional.get();
}
private WxPayMrchCfg getApiV3Cfg(String merchantId) {
String merchantIdCfg = redisUtils.getHashValue(REDIS_KEY_RCHG_PARTNER_PAY_MRCH_CFG, merchantId);
if (StrUtil.isBlank(merchantIdCfg)) {
throw new RuntimeException("查询不到此微信商户号ApiV3配置:" + merchantId);
}
WxPayMrchCfg wxPayMrchCfg = JSON.parseObject(merchantIdCfg, WxPayMrchCfg.class);
if (!doCheckParam(merchantId, wxPayMrchCfg.getCertSerialNo(), wxPayMrchCfg.getPartnerKey(),
wxPayMrchCfg.getCertPrivateKeyPath())) {
throw new RuntimeException("初始化 CloseableHttpClient 失败,缺少必要参数");
}
return wxPayMrchCfg;
}
private Verifier getVerifier(String merchantId, String merchantSerialNumber, String apiV3Key,
PrivateKey merchantPrivateKey)
throws IOException, GeneralSecurityException, HttpCodeException, NotFoundException {
Verifier verifier;
try {
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(merchantId);
} catch (IllegalArgumentException | NotFoundException e) {
// 向证书管理器增加需要自动更新平台证书的商户信息
certificatesManager.putMerchant(merchantId,
new WechatPay2Credentials(merchantId,
new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8));
// 从证书管理器中获取verifier
verifier = certificatesManager.getVerifier(merchantId);
} catch (Exception e) {
throw new RuntimeException("获取verifier失败");
}
return verifier;
}
private Optional<CloseableHttpClient> getHttpClient(String merchantId, String merchantSerialNumber,
PrivateKey merchantPrivateKey, Verifier verifier) {
if (MERCHANT_ID_HTTP_CLIENT_MAP.get(merchantId) == null) {
synchronized (WxApiV3Utils.class) {
if (MERCHANT_ID_HTTP_CLIENT_MAP.get(merchantId) == null) {
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient newHttpClient = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
MERCHANT_ID_HTTP_CLIENT_MAP.put(merchantId, newHttpClient);
}
}
}
return Optional.ofNullable(MERCHANT_ID_HTTP_CLIENT_MAP.get(merchantId));
}
public BizResponse request(CloseableHttpClient httpClient, HttpPost httpPost, String reqMethodName)
throws IOException {
RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000).setConnectionRequestTimeout(2000)
.setSocketTimeout(5000).build();
httpPost.setConfig(requestConfig);
BizResponse bizResponse;
int statusCode;
// 完成签名并执行请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { // 处理成功
bizResponse = BizResponse.success(EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) {// 处理成功,无返回Body
bizResponse = BizResponse.success("success");
} else {
bizResponse = BizResponse.failure(EntityUtils.toString(response.getEntity()));
}
}
logger.info("{} 返回状态码:{},数据:{}", reqMethodName, statusCode, bizResponse.getMsg());
return bizResponse;
}
/**
* 获取微信支付平台证书序列号
*/
public String getPlatformCertificateSerial(String merchantId) throws NotFoundException {
return certificatesManager.getVerifier(merchantId).getValidCertificate().getSerialNumber().toString(16);
}
/**
* 敏感信息加密
*/
public String sensitiveInfoEncrypt(String merchantId, String text) throws Exception {
Verifier verifier = CertificatesManager.getInstance().getVerifier(merchantId);
X509Certificate certificate = verifier.getValidCertificate();
return RsaCryptoUtil.encryptOAEP(text, certificate);
}
private boolean doCheckParam(String mrchId, String certSerialNo, String apiV3Key, String privateKeyFilePath) {
return doCheckValue(mrchId, certSerialNo, apiV3Key, privateKeyFilePath);
}
private boolean doCheckValue(String... item) {
for (String param : item) {
if (StrUtil.isBlank(param)) {
return false;
}
}
return true;
}
}
例如请求支付即服务的api可以看下面示例
/**
* 支付即服务 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_4_1.shtml
*
*/
@Component
public class WxSmartGuideApi {
private static final Logger logger = LoggerFactory.getLogger(WxSmartGuideApi.class);
@Autowired
private WxApiV3Utils wxApiV3Utils;
/**
* 服务人员分配
*
* @param merchantId
* 商户id
* @param guideId
* 服务人员ID
* @param outOrderNo
* 商户订单号
*/
public BizResponse assignGuide(String merchantId, String guideId, String outOrderNo) throws Exception {
CloseableHttpClient httpClient = wxApiV3Utils.getHttpClient(merchantId);
// 请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/smartguide/guides/" + guideId + "/assign");
// 请求body参数
JSONObject body = new JSONObject();
body.put("out_trade_no", outOrderNo);
logger.info("服务人员分配 url:{},body:{}", httpPost.getURI(), body);
initHttpPost(body, httpPost);
return wxApiV3Utils.request(httpClient, httpPost, "服务人员分配");
}
private void initHttpPost(JSONObject body, HttpPost httpPost) {
StringEntity entity = new StringEntity(body.toString(), "utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
}
/**
* 服务人员注册
*/
public BizResponse regSmartGuide(String merchantId, String corpId, int storeId, String userId, String mobile,
String qrCode, String avatar, String name) throws Exception {
CloseableHttpClient httpClient = wxApiV3Utils.getHttpClient(merchantId);
// 请求URL
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/smartguide/guides");
// 请求body参数
JSONObject body = new JSONObject();
body.put("corpid", corpId);
body.put("store_id", storeId);
body.put("userid", userId);
body.put("name", wxApiV3Utils.sensitiveInfoEncrypt(merchantId, name));
body.put("mobile", wxApiV3Utils.sensitiveInfoEncrypt(merchantId, mobile));
body.put("qr_code", qrCode);
body.put("avatar", avatar);
logger.info("服务人员注册 url:{},body:{}", httpPost.getURI(), body);
initHttpPost(body, httpPost);
String wechatpaySerial = wxApiV3Utils.getPlatformCertificateSerial(merchantId);
httpPost.setHeader("Wechatpay-Serial", wechatpaySerial);
return wxApiV3Utils.request(httpClient, httpPost, "服务人员注册");
}
}