白嫖之微信支付V3 java restful
前言
疫情创业失败,回来打工了。。最近所在公司新做了一个小程序,涉及到支付功能,一看文档,居然出了新的v3 restful的API(原谅我很久没写代码了),记得很久之前写过v2的xml格式的,很是痛苦。最后决定用新的方式调用,搞了两天,终于通了,确实比老版的简单,但网上的整体代码文档很少,在这里跟大家分享下,供大家白嫖。
从未写过的同学,一步一步跟着走即可实现小程序微信支付V3统一下单、支付回调、验签功能。
微信支付调用流程很简单,大家看文档学习即可。
官方文档
新版支付核心概念-非对称加密
为啥要加密,这里不解释了。。大家应该都知道。
大家只要记住下面八个概念即可
1、四个动作,加密解密,签名验签(签名也是加过密的)
2、四个钥匙,我的公私钥,微信的公私钥
公钥是我给大家的,私钥是我自己保存好的,两个钥匙是一对,负责双方的加解密
我方加密为了加密传输信息,对方解密为了解密传输信息
我方签名是在我的请求上,签个名,为了证明来源是我本人
对方验签就是验证我的签名对不对,不对的话说明请求是别人假冒的
对应关系为
我的私钥用来签名,微信用我的公钥验证签名
我用微信的公钥加密传输信息,微信用自己的私钥解密信息
反之
微信用私钥签名,我用微信的公钥验证签名
微信用我得公钥加密信息,我用自己私钥解密信息
所以,我们和微信双方,都各自需要有3个钥匙,自己的私钥,自己的公钥,对方的公钥
大家可能发现了,公钥要发送给别人的公开的,被伪造了怎么办?
于是有了CA,主要作用就是给我们的证书做保证,保证是真。CA也是有更上一层机构去保证,大家有兴趣可以自行阅读。
以上信息请大家仔细阅读并理解,个人认为其为微信支付的关键,搞懂加密流程,其余就是并发与业务处理,暂不在本文章讨论范围。
概念说完了
我们要进入正式的微信支付了
第一步:获取自己的公私钥,把公钥给微信,设置APIv3密钥
我已经默认大家注册了商户号与小程序
登陆商户后台,上面账户中心,左侧API安全
下载微信证书工具,按照步骤即可下载自己的公私钥和证书信息,压缩包如下
然后将生成的公钥,复制粘贴到微信商户后台。至此
我们有了自己的公私钥,微信有自己公私钥和我们的公钥,我们还差一个微信的公钥,后面再讲。
第二步:准备微信支付所需参数
1、将下载的证书apiclient_cert.p12,随便放在项目里,我这里放在了src/main/resources
2、准备以下参数
商户id
自己证书id
小程序appid
支付后微信回调地址
自己设置的apiV3Key
以上参数都可以在小程序后台或者商户后台获取到
第三步:工具类WxPayUtil准备
开始白嫖代码,懒得看直接复制即可
整体工具类在附录
1、加载证书获取公私钥,即我们复制在src/main/resources下的apiclient_cert.p12
private KeyStore store;
private final Object lock = new Object();
public KeyPair createPKCS12(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
certificate.checkValidity();
// 证书的序列号
// String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
// 证书的 公钥
PublicKey publicKey = certificate.getPublicKey();
// 证书的私钥
PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);
return new KeyPair(publicKey, storeKey);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
2、随机串
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
public 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);
}
3、时间戳
long timestamp = System.currentTimeMillis()/1000;
4、请求参数body体,封装了三个DTO,大家可自行修改增加字段
public class WxPrePayMainDTO
private String appid;
private String mchid;
private String description;
private String out_trade_no;
private String attach;
private String notify_url;
private WxPrePayAmountDTO amount;
private WxPrePayPayerDTO payer;
public class WxPrePayAmountDTO
//单位是分
private Integer total;
private String currency = "CNY";
public class WxPrePayPayerDTO
private String openid;
至此我们基本上是准备完成了
第四步:签名、token、测试下载证书接口
签名规则请仔细阅读文档,个人觉得这个文档还是很详细的。
签名生成
1、签名方法
@SneakyThrows
public String requestSign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) {
String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(keyPair.getPrivate());
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
我们最后需要的是token
2、用签名生成token(规则请参考微信文档)
public String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) {
final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\","
+ "timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
// 生成token
return String.format(TOKEN_PATTERN,
mchId,
nonceStr, timestamp, serialNo, signature);
}
3、先用get方法试一下,ok不ok
试下这个接口,下载证书
public static void main(String[] args) {
WxPayUtil KeyPairFactory = new WxPayUtil();
KeyPair createPKCS12 = KeyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", "商户号");
String nonceStr = KeyPairFactory.generateNonceStr();
long timestamp = System.currentTimeMillis()/1000;
String sign = KeyPairFactory.requestSign("GET", "/v3/certificates",
timestamp, nonceStr,
""
, createPKCS12);
String token = KeyPairFactory.token("商户号", nonceStr, timestamp,
"证书号", sign);
System.out.println(token);
}
将生成的token,放在header里,如果返回401说明签名失败,如果配置没有问题,则200
第五步:下单,返给前端签名信息
@Override
public ResponseResult wxToPay(String userToken,Order order) {
UserVO userVO = userService.findUserVOByToken(userToken);
if(userVO==null){
return ResponseResult.failure(ResponseResultEnum.no_user);
}
order.setUid(userVO.getId());
order.setDescription("爱心捐赠");
order.setCode(this.createOrderCode());//自己实现业务订单
//建造者模式构建参数
WxPrePayMainDTO wxPrePayMainDTO = new WxPrePayDTOBuilder()
.appid(appid)
.mchid(mchid)
.description(order.getDescription())
.out_trade_no(order.getCode())
.attach("")
.notify_url("https://www.baidu.com/")
.amount(new WxPrePayAmountDTO(order.getAmount().multiply(new BigDecimal(100))))
.payer(new WxPrePayPayerDTO(userVO.getOpenid()))
.build();
//获取token
String token = this.getToken(JSON.toJSONString(wxPrePayMainDTO), "POST", "/v3/pay/transactions/jsapi");
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type","application/json");
headers.set("Authorization", token);
System.out.println(JSON.toJSONString(wxPrePayMainDTO));
System.out.println(token);
HttpEntity<String> request = new HttpEntity<String>(
JSON.toJSONString(wxPrePayMainDTO), headers);
try {
JSONObject response = restTemplate.postForObject("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",
request, JSONObject.class);
String prepay_id = response.getString("prepay_id");
if(null == prepay_id) {
return ResponseResult.failure(ResponseResultEnum.pre_pay_error);
}
//需要再次签名,返给前端用来唤起支付
System.out.println(response);
orderMapper.insert(order);
AwakenPayDTO awakenPayDTO = this.getAwakenPaySign("prepay_id="+prepay_id);
awakenPayDTO.setOrderCode(order.getCode());
return ResponseResult.success(awakenPayDTO);
} catch (HttpStatusCodeException e) {
System.out.println( e.getStatusCode());
System.out.println( e.getResponseBodyAsString());
System.out.println( e.getResponseHeaders());
}
return ResponseResult.failure(ResponseResultEnum.pre_pay_error);
}
private String getToken(String body,String method,String url) {
//1.加载证书
KeyPair keyPair = wxPayUtil.createPKCS12("/apiclient_cert.p12",
"Tenpay Certificate", mchid);
String nonceStr = wxPayUtil.generateNonceStr();
long timestamp = System.currentTimeMillis()/1000;
System.out.println(url);
//2.获取签名
String sign = wxPayUtil.requestSign(method, url, timestamp, nonceStr,
body,keyPair);
//3.封装token
String token = wxPayUtil.token(mchid,nonceStr,timestamp,certno, sign);
return token;
}
private AwakenPayDTO getAwakenPaySign(String body) {
//1.加载证书
KeyPair keyPair = wxPayUtil.createPKCS12("/apiclient_cert.p12",
"Tenpay Certificate", mchid);
AwakenPayDTO awakenPayDTO = new AwakenPayDTO();
String nonceStr = wxPayUtil.generateNonceStr();
long timestamp = System.currentTimeMillis()/1000;
//2.获取签名
String sign = wxPayUtil.awakenPaySign(appid, timestamp, nonceStr, body, keyPair);
//3.封装token
awakenPayDTO.setNonceStr(nonceStr);
awakenPayDTO.setPackageInfo(body);
awakenPayDTO.setSignType("RSA");
awakenPayDTO.setTimeStamp(timestamp);
awakenPayDTO.setPaySign(sign);
return awakenPayDTO;
}
public class AwakenPayDTO
private long timeStamp;
private String nonceStr;
private String packageInfo;
private String signType;
private String paySign;
private String orderCode;
剩下的就是前端的事情啦
附录
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import lombok.SneakyThrows;
@Component
public class WxPayUtil {
private KeyStore store;
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
private final Object lock = new Object();
/**
* 获取公私钥.
*
* @param keyPath the key path
* @param keyAlias the key alias
* @param keyPass password
* @return the key pair
*/
public KeyPair createPKCS12(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("PKCS12");
store.load(resource.getInputStream(), pem);
}
}
}
X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias);
certificate.checkValidity();
// 证书的序列号
// String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
// 证书的 公钥
PublicKey publicKey = certificate.getPublicKey();
// 证书的私钥
PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem);
return new KeyPair(publicKey, storeKey);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}
}
/**
* V3 SHA256withRSA 签名.
*
* @param method 请求方法 GET POST PUT DELETE 等
* @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1
* @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致
* @param nonceStr 随机字符串 要和TOKEN中的保持一致
* @param body 请求体 GET 为 "" POST 为JSON
* @param keyPair 商户API 证书解析的密钥对 实际使用的是其中的私钥
* @return the string
*/
@SneakyThrows
public String requestSign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) {
String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(keyPair.getPrivate());
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
@SneakyThrows
public String awakenPaySign(String appid, long timestamp, String nonceStr, String body, KeyPair keyPair) {
String signatureStr = Stream.of(appid,String.valueOf(timestamp), nonceStr, body)
.collect(Collectors.joining("\n", "", "\n"));
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(keyPair.getPrivate());
sign.update(signatureStr.getBytes(StandardCharsets.UTF_8));
return Base64Utils.encodeToString(sign.sign());
}
/**
* 生成Token.
*
* @param mchId 商户号
* @param nonceStr 随机字符串
* @param timestamp 时间戳
* @param serialNo 证书序列号
* @param signature 签名
* @return the string
*/
public String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) {
final String TOKEN_PATTERN = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\","
+ "timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";
// 生成token
return String.format(TOKEN_PATTERN,
mchId,
nonceStr, timestamp, serialNo, signature);
}
public 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);
}
/**
* 解密响应体.
*
* @param apiV3Key API V3 KEY API v3密钥 商户平台设置的32位字符串
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
* @throws GeneralSecurityException the general security exception
*/
public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
public static void main(String[] args) {
WxPayUtil KeyPairFactory = new WxPayUtil();
KeyPair createPKCS12 = KeyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", "商户号");
String nonceStr = KeyPairFactory.generateNonceStr();
long timestamp = System.currentTimeMillis()/1000;
String sign = KeyPairFactory.requestSign("GET", "/v3/certificates",
timestamp, nonceStr,
""
, createPKCS12);
String token = KeyPairFactory.token("商户号", nonceStr, timestamp,
"证书号", sign);
System.out.println(token);
}
}
微信支付我只写了两天,比较匆忙,希望能给大家帮助