微信支付API-V3和之前V2版本最大的区别,应该就是加密方式的改变了。新版的支付接口,全部使用是SSL双向加密。就是指微信服务器端、商户端各自都有一套证书,两者之间通讯必需使用自己证书的私钥加密,使用对方的公钥解密。具体流程图,可以参考微信支付官网的这张图:
上图所在的文档链接是:
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml 有需要的可参考。
由于微信支付官方提供的JAVA Demo使用的是Httpclient,并且比较庞杂。所以我自己对接的时候,是使用OkHttp完成的。
每种支付方式的准备都稍有区别,但区别也不大。这里以JS-API支付为例,简单说说一个全新的微信支付账号,要做哪些配置。
以上就是接入前准备的简单介绍,具体每一步的详细操作,可以参考微信支付官方的文档:
微信支付本身对接起来不麻烦,无非就是下单、支付、等通知该状态三步。对新手不友好的地方,主要还是各种加密、签名等安全措施。接下来我介绍一下如何使用OkHttp封装一个http请求类,包含各种安全验证措施,外部调用的时候,只要当普通OkHttp接口调用就行。各种权限验证,已经在类里面自己实现了。
简单介绍一下封装类的各个方法:
完整代码如下,代码依赖了很多常见的类库,比如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);
}
//开始拼接Token
return 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 = 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<WxPlatCertDO> decodeWxPlatCert(String json) {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode;
JsonNode dataNode;
try {
jsonNode = mapper.readTree(json);
dataNode = jsonNode.get("data");
} catch (Exception e) {
throw new RuntimeException("读取证书JSON失败", e);
}
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
List<WxPlatCertDO> certList = new ArrayList<>();
for (JsonNode itemNode : dataNode) {
JsonNode certNode = itemNode.get("encrypt_certificate");
String cert = AesUtil.decryptJsonNodeToString(certNode);
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8))
);
x509Cert.checkValidity();
WxPlatCertDO wxPlatCertDO = new WxPlatCertDO();
wxPlatCertDO.setCertificate(x509Cert);
wxPlatCertDO.setCert(cert);
wxPlatCertDO.setEffectiveTime(format.parse(itemNode.get("effective_time").asText()));
wxPlatCertDO.setExpireTime(format.parse(itemNode.get("expire_time").asText()));
wxPlatCertDO.setSerialNumber(itemNode.get("serial_no").asText());
certList.add(wxPlatCertDO);
} catch (Exception e) {
logger.error("update wx plat cert err", e);
}
}
return certList;
}
/**
* 封装的http get请求,直接使用即可,证书、签名的生成和校验已经集成
* @param url
* @param headerMap
* @return
*/
public static String wxGet(String url, HashMap<String, String> headerMap) {
boolean lazyVerify = false;
if (url.equals(CERT_LIST)) {
//是下载证书的请求,允许延迟验签
lazyVerify = true;
}
String token = generateToken(url, "get", null);
headerMap = intHeader(headerMap);
headerMap.put("Authorization", token);
Request.Builder builder = new Request.Builder().url(url);
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
Request request = builder.build();
try (Response response = getClient().newCall(request).execute()) {
return checkResponseSign(response, lazyVerify);
} catch (IOException e) {
throw new RuntimeException("wx okHttp get err", e);
}
}
/**
* 封装的http post请求,直接使用即可,证书、签名的生成和校验已经集成
* @param url
* @param json
* @param headerMap
* @return
*/
public static String wxPost(String url, String json, HashMap<String, String> headerMap) {
RequestBody requestBody = RequestBody.create(json, MediaType.parse("application/json"));
String token = generateToken(url, "post", json);
headerMap = intHeader(headerMap);
headerMap.put("Authorization", token);
Request.Builder builder = new Request.Builder().url(url).post(requestBody);
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
try (Response response = getClient().newCall(builder.build()).execute()) {
return checkResponseSign(response, false);
} catch (IOException e) {
throw new RuntimeException("wx okHttp post err", e);
}
}
/**
* 从微信下载微信平台证书
* @return
*/
public static List<WxPlatCertDO> getCertList() {
String s = wxGet(CERT_LIST, null);
return decodeWxPlatCert(s);
}
}
上面的代码中,依赖了很多我自己写的其他类库,这里逐一介绍一下。
package com.coderbbb.blogv2.utils;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.NET.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class OkhttpUtil {
private static OkHttpClient client = null;
private static final Logger logger = LoggerFactory.getLogger(OkhttpUtil.class);
private synchronized static void createClient() {
if (client == null) {
OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
okHttpBuilder.protocols(Collections.singletonList(Protocol.HTTP_1_1));
okHttpBuilder.connectTimeout(60, TimeUnit.SECONDS);
okHttpBuilder.readTimeout(60, TimeUnit.SECONDS);
okHttpBuilder.writeTimeout(60, TimeUnit.SECONDS);
okHttpBuilder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//支持所有类型https请求
return true;
}
});
ConnectionPool pool = new ConnectionPool(200, 1, TimeUnit.SECONDS);
okHttpBuilder.connectionPool(pool);
client = okHttpBuilder.build();
client.dispatcher().setMaxRequests(2000);
client.dispatcher().setMaxRequestsPerHost(1000);
}
}
public static OkHttpClient getClient() {
if (client == null) {
createClient();
}
return client;
}
public static String get(String url, HashMap<String, String> headerMap) {
Request.Builder builder = new Request.Builder().url(url);
if (headerMap != null) {
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
}
Request request = builder.build();
String result = null;
try {
result = excute(request);
} catch (Exception e) {
logger.warn("http get fail:" + url + "###" + e.getMessage());
}
return result;
}
public static String postRaw(String url, String contentType, String json, HashMap<String, String> headerMap) throws Exception {
RequestBody requestBody = RequestBody.create(json,MediaType.parse(contentType));
Request.Builder builder = new Request.Builder().url(url).post(requestBody);
if (headerMap != null) {
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
}
Request request = builder.build();
String result = null;
try {
result = excute(request);
} catch (Exception e) {
logger.error("http post raw fail:" + url + "###" + e.getMessage());
}
return result;
}
public static String post(String url, HashMap<String, String> data, HashMap<String, String> headerMap) throws Exception {
FormBody.Builder formBodyBuilder = new FormBody.Builder();
for (Map.Entry<String, String> entry : data.entrySet()) {
formBodyBuilder.add(entry.getKey(), entry.getValue());
}
RequestBody requestBody = formBodyBuilder.build();
Request.Builder builder = new Request.Builder().url(url).post(requestBody);
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
Request request = builder.build();
String result = null;
try {
result = excute(request);
} catch (Exception e) {
logger.error("http post fail:" + url + "###" + e.getMessage());
}
return result;
}
private static String excute(Request request) throws Exception {
Response response = getClient().newCall(request).execute();
ResponseBody body = response.body();
if (body != null) {
String str = body.string();
body.close();
response.close();
return str;
}
return null;
}
}
package com.coderbbb.blogv2.utils;
import com.fasterxml.jackson.databind.JsonNode;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesUtil {
private static final int TAG_LENGTH_BIT = 128;
public static String aesKey;
public static String decryptJsonNodeToString(JsonNode jsonNode){
return decryptToString(
jsonNode.get("associated_data").asText().getBytes(StandardCharsets.UTF_8),
jsonNode.get("nonce").asText().getBytes(StandardCharsets.UTF_8),
jsonNode.get("ciphertext").asText());
}
public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) {
if (aesKey == null) {
throw new RuntimeException("aesKey不能为空");
}
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("aes解密失败", e);
}
}
}
package com.coderbbb.blogv2.utils;
import com.coderbbb.blogv2.database.dto.WxCertDataDTO;
import org.apache.commons.codec.binary.Base64;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
/**
* 微信支付证书解析
*
* @author longge93
*/
public class WxCertUtil {
private static WxCertDataDTO wxCertDataDTO = null;
public static String keyPass = null;
public static String sslPath = "ssl/wx2.p12";
private static synchronized WxCertDataDTO loadCert() {
if (wxCertDataDTO != null) {
return wxCertDataDTO;
}
if (keyPass == null) {
throw new RuntimeException("还没有设置证书密码");
}
ClassPathResource classPathResource = new ClassPathResource(sslPath);
String serialNumber;
PublicKey publicKey;
PrivateKey privateKey;
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(classPathResource.getInputStream(), keyPass.toCharArray());
Enumeration<String> aliases = keyStore.aliases();
X509Certificate cert = (X509Certificate) keyStore.getCertificate(aliases.nextElement());
//证书序列号
serialNumber = cert.getSerialNumber().toString(16).toUpperCase();
// 证书公钥
publicKey = cert.getPublicKey();
// 证书私钥
privateKey = (PrivateKey) keyStore.getKey(keyStore.getCertificateAlias(cert), keyPass.toCharArray());
} catch (Exception e) {
throw new RuntimeException("读取证书失败", e);
}
wxCertDataDTO = new WxCertDataDTO();
wxCertDataDTO.setPublicKey(publicKey);
wxCertDataDTO.setPrivateKey(privateKey);
wxCertDataDTO.setSerialNumber(serialNumber);
wxCertDataDTO.setMchId(keyPass);
return wxCertDataDTO;
}
public static WxCertDataDTO getCert() {
if (wxCertDataDTO != null) {
return wxCertDataDTO;
}
return loadCert();
}
public static String rsaSign(String signatureStr){
WxCertDataDTO wxCertDataDTO = getCert();
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(wxCertDataDTO.getPrivateKey());
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(sign.sign());
} catch (Exception e) {
throw new RuntimeException("RSA签名失败");
}
}
}
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
到这一步,微信支付API-V3的各种安全校验问题应该是解决了,专栏下一篇就介绍怎么使用本文封装好的HTTP GET、HTTP POST来完成整个微信支付流程。
package com.coderbbb.book1.database.dto;
import lombok.Data;
import java.security.PrivateKey;
import java.security.PublicKey;
@Data
public class WxCertDataDTO {
/**
* 证书序列号
*/
private String serialNumber;
/**
* 证书公钥
*/
private PublicKey publicKey;
/**
* 证书私钥
*/
private PrivateKey privateKey;
/**
* 商户号
*/
private String mchId;
}
package com.coderbbb.blogv2.database.dos;
import lombok.Data;
import java.security.cert.X509Certificate;
import java.util.Date;
@Data
public class WxPlatCertDO extends MybatisBaseDO{
/**
* 证书序列号
*/
private String serialNumber;
/**
* 证书过期时间
*/
private Date expireTime;
/**
* 生效时间
*/
private Date effectiveTime;
/**
* X509Certificate JSON序列化
*/
private String cert;
/**
* 证书
*/
private X509Certificate certificate;
}
版权声明:《Java Springboot使用OkHttp实现微信支付API-V3签名、证书的管理和使用》为CoderBBB作者「ʘᴗʘ」的原创文章,转载请附上原文出处链接及本声明。
原文链接:
https://www.coderbbb.com/articles/4