Springboot使用OkHttp实现微信支付API-V3签名、证书的管理和使用

微信支付API-V3和V2的区别微信支付API-V3和之前V2版本最大的区别 , 应该就是加密方式的改变了 。新版的支付接口 , 全部使用是SSL双向加密 。就是指微信服务器端、商户端各自都有一套证书 , 两者之间通讯必需使用自己证书的私钥加密 , 使用对方的公钥解密 。具体流程图 , 可以参考微信支付官网的这张图:

Springboot使用OkHttp实现微信支付API-V3签名、证书的管理和使用

文章插图
 
上图所在的文档链接是:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml 有需要的可参考 。
由于微信支付官方提供的JAVA Demo使用的是Httpclient , 并且比较庞杂 。所以我自己对接的时候 , 是使用OkHttp完成的 。
接入前准备每种支付方式的准备都稍有区别 , 但区别也不大 。这里以JS-API支付为例 , 简单说说一个全新的微信支付账号 , 要做哪些配置 。
  1. 绑定App ID和商户号mch id:微信支付申请下来后 , 是没办法单独使用的 。肯定要依托于公众号、小程序、APP或者网站等载体 , 这些载体都有自己的APP ID , 需要在这些载体对应的后台里 , 找到微信支付菜单 , 点进去把微信支付的mch id和app id绑定起来 。
  2. 设置API KEY:这个key主要用来解密一些微信接口的返回结果 , 比如下载微信平台证书的时候 。(为什么在双向证书的情况下 , 还需要这个key呢?因为这些证书 , 只使用来签名的 , 并不能加密每次请求的body)
  3. 下载商户证书:这个没啥好说的 , 新版的微信支付V3接口 , 商户和微信平台 , 各自都有证书 。
  4. 配置各种授权域名、授权目录等 。
以上就是接入前准备的简单介绍 , 具体每一步的详细操作 , 可以参考微信支付官方的文档:
  • JSAPI接入前准备:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml
  • APP支付接入前准备https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_1.shtml
  • 小程序支付接入前准备https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml
使用OkHttp封装自带微信支付API-V3证书加解密、签名的请求微信支付本身对接起来不麻烦 , 无非就是下单、支付、等通知该状态三步 。对新手不友好的地方 , 主要还是各种加密、签名等安全措施 。接下来我介绍一下如何使用OkHttp封装一个http请求类 , 包含各种安全验证措施 , 外部调用的时候 , 只要当普通OkHttp接口调用就行 。各种权限验证 , 已经在类里面自己实现了 。
简单介绍一下封装类的各个方法:
  • generateToken :当我们请求微信的接口的时候 , 首先得生成签名信息 , 放到HTTP请求的Header里 , 名字叫Authorization 。
  • checkResponseSign :拿到微信的返回结果后 , 我们得拿返回结果算一下签名 , 然后和返回的签名对比一下 , 看看这个请求结果是真的还是伪造的 。
  • decodeWxPlatCert :微信的平台证书 , 定期会自动更新 , 我们需要调用接口下载微信的平台证书回来 。这个接口的返回结果 , 是用我们前面“接入前准备”中提到的API KEY加密的 , 所以 , 我们得用这个方法解密 。
  • wxGet wxPost :这两个就是封装好的HTTP GET和HTTP POST请求了 , 已经在内部实现了各种安全措施 。
完整代码如下 , 代码依赖了很多常见的类库 , 比如Apache-commons-lang3等 , 可以从代码的import中看出来 , 自行添加maven pom 。
package com.coderbbb.blogv2.utils;import com.coderbbb.blogv2.database.dos.WxPlatCertDO;import com.coderbbb.blogv2.database.dto.WxCertDataDTO;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import okhttp3.*;import org.apache.commons.codec.binary.Base64;import org.apache.commons.lang3.RandomStringUtils;import org.apache.commons.lang3.StringUtils;import org.apache.http.HttpStatus;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.servlet.http.HttpServletRequest;import java.io.ByteArrayInputStream;import java.io.IOException;import java.nio.charset.StandardCharsets;import java.security.Signature;import java.security.cert.CertificateFactory;import java.security.cert.X509Certificate;import java.text.SimpleDateFormat;import java.util.*;import java.util.concurrent.ConcurrentHashMap;import java.util.stream.Collectors;import java.util.stream.Stream;public class WxOkHttpUtil extends OkhttpUtil {private static final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"";private static final String CERT_LIST = "https://api.mch.weixin.qq.com/v3/certificates";public static ConcurrentHashMap<String, WxPlatCertDO> WX_PLAT_CERT = new ConcurrentHashMap<>();private final static Logger logger = LoggerFactory.getLogger(WxOkHttpUtil.class);/*** 生成请求微信接口时需要的Authorization头* @param url* @param method* @param json* @return*/public static String generateToken(String url, String method, String json) {if (json == null) {json = "";}url = url.substring(StringUtils.ordinalIndexOf(url, "/", 3));long timestamp = System.currentTimeMillis() / 1000;String timestampStr = String.valueOf(timestamp);String nonceStr = RandomStringUtils.random(16, true, true);String signatureStr = Stream.of(method.toUpperCase(Locale.ROOT), url, timestampStr, nonceStr, json).collect(Collectors.joining("n", "", "n"));WxCertDataDTO wxCertDataDTO = WxCertUtil.getCert();String signResult;try {Signature sign = Signature.getInstance("SHA256withRSA");sign.initSign(wxCertDataDTO.getPrivateKey());sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));signResult = Base64.encodeBase64String(sign.sign());} catch (Exception e) {throw new RuntimeException("签名失败", e);}//开始拼接Tokenreturn String.format(TOKEN_PATTERN, wxCertDataDTO.getMchId(), nonceStr, timestamp, wxCertDataDTO.getSerialNumber(), signResult);}/*** 请求基本Header头 , 微信规定的 。* 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml* @param headerMap* @return*/private static HashMap<String, String> intHeader(HashMap<String, String> headerMap) {if (headerMap == null) {headerMap = new HashMap<>();}headerMap.put("content-type", "application/json;charset=UTF-8");headerMap.put("user-agent", "coderbbb");headerMap.put("accept", "application/json");return headerMap;}/*** 微信请求我们时(比如支付的异步通知) , 拿这个函数校验微信的请求是否是合法的* 简单说 , 就是验证微信请求我们时 , 签名是否正确的 。* @param request* @param requestBody* @return*/public static boolean checkServletRequestSign(HttpServletRequest request, String requestBody) {String wxCertSerialNumber = request.getHeader("Wechatpay-Serial");String timestamp = request.getHeader("Wechatpay-Timestamp");String nonce = request.getHeader("Wechatpay-Nonce");String sign = request.getHeader("Wechatpay-Signature");if (!WX_PLAT_CERT.containsKey(wxCertSerialNumber)) {return false;}WxPlatCertDO wxPlatCertDO = WX_PLAT_CERT.get(wxCertSerialNumber);String signBody = Stream.of(timestamp, nonce, requestBody).collect(Collectors.joining("n", "", "n"));//开始验签try {Signature signature = Signature.getInstance("SHA256withRSA");signature.initVerify(wxPlatCertDO.getCertificate());signature.update(signBody.getBytes(StandardCharsets.UTF_8));if (!signature.verify(Base64.decodeBase64(sign))) {return false;}} catch (Exception e) {logger.error("验签错误", e);return false;}return true;}/*** 我们请求微信的接口后 , 得到返回结果 , 用这个函数把返回结果生成签名 , 和返回的签名对比是否一致* 简单说 , 就是请求微信的接口后 , 我们自己用返回结果生成一个签名 , 和请求返回的签名对比 , 看看签名一样不* @param response* @param lazyVerify* @return*/private static String checkResponseSign(Response response, boolean lazyVerify) {String result = null;String wxCertSerialNumber;String timestamp;String nonce;String sign;try (ResponseBody body = response.body();) {if (body != null) {result = body.string();}if (response.code() != 200) {logger.warn("err response = " + result);throw new RuntimeException("请求http code异常:" + response.code());}wxCertSerialNumber = response.header("Wechatpay-Serial");timestamp = response.header("Wechatpay-Timestamp");nonce = response.header("Wechatpay-Nonce");sign = response.header("Wechatpay-Signature");} catch (IOException e) {throw new RuntimeException("wx okHttp read response err", e);}WxPlatCertDO wxPlatCertDO = null;if (!WX_PLAT_CERT.containsKey(wxCertSerialNumber)) {//平台证书不在已有的列表内/*** 我们提供以下的机制 , 帮助商户在平台证书更新时实现平滑切换:** 1.下载新平台证书 。我们将在旧证书过期前10天生成新证书 。* 商户可使用平台证书下载API 下载新平台证书 , 并在旧证书过期前5-10天部署新证书 。** 2.兼容使用新旧平台证书 。旧证书过期前5天至过期当天 , 新证书开始逐步放量用于应答和回调的签名 。* 商户需根据证书序列号 , 使用对应版本的平台证书 。* (我们在所有API应答和回调的HTTP头部Wechatpay-Serial , 声明了此次签名所对应的平台证书的序列号 。)*///所以:定时任务拉取微信平台证书 , 在这里 , 如果证书不在列表内 , 只有两种情况://1.该请求是第一次下载微信平台证书的请求;2.恶意请求 。// - 我们使用lazyVerify标记第一种情况if (!lazyVerify) {//不能延迟验签 , 抛出错误throw new RuntimeException("签名校验失败");} else {//可以延迟验签 , 说明该请求是下载微信平台证书的请求 , 直接读取请求返回值 , 提取证书List<WxPlatCertDO> wxPlatCertData = https://www.isolves.com/it/cxkf/kj/2021-12-29/decodeWxPlatCert(result);for (WxPlatCertDO item : wxPlatCertData) {if (item.getSerialNumber().equals(wxCertSerialNumber)) {wxPlatCertDO = item;}}}} else {wxPlatCertDO = WX_PLAT_CERT.get(wxCertSerialNumber);}if (wxPlatCertDO == null) {throw new RuntimeException("平台证书不存在 , 验签失败");}String signBody = Stream.of(timestamp, nonce, result).collect(Collectors.joining("n", "", "n"));//开始验签try {Signature signature = Signature.getInstance("SHA256withRSA");signature.initVerify(wxPlatCertDO.getCertificate());signature.update(signBody.getBytes(StandardCharsets.UTF_8));if (!signature.verify(Base64.decodeBase64(sign))) {throw new RuntimeException("签名错误 , 请求不安全");}} catch (Exception e) {throw new RuntimeException("验签错误", e);}return result;}/*** 下载微信平台证书时 , 用这个函数解密拿到的返回值 , 得到微信平台证书列表* @param json* @return*/private static List


推荐阅读