目录
相关官网文档
1.需要的参数
# appId
wechat.appid=${WECHAT_APPID}
# 商户号
wechat.mchid=${WECHAT_MCHID}
# 证书序列号
wechat.mch.certno=${WECHAT_CERTNO}
# APIv3密钥
wechat.pay.api-v3-key=${WECHAT_V3KEY}
证书下载,apiclient_cert.p12,放到resource目录下
需要的参数和证书,根据接入前准备官方文档获取。
2.引入库
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.2</version>
</dependency>
3.用到的工具类
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Grace
*/
@Component
public class KeyPairFactory {
@Value("${wechat.mchid}")
private String MCHID;
@Value("${wechat.mch.certno}")
private String CERTNO;
@Value("${wechat.pay.api-v3-key}")
private String V3KEY;
private KeyStore store;
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
private final Object lock = new Object();
private static final Map<String, Object> VERIFIER_MAP = new ConcurrentHashMap<>();
/**
* 获取公私钥.
*
* @param keyPath API证书apiclient_cert.p12的classpath路径
* @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);
}
}
/**
* 构造验签名串.
*
* @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。
* @param wechatpayNonce HTTP头 Wechatpay-Nonce 中的应答随机串
* @param body 响应体
* @return the string
*/
public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) {
return Stream.of(wechatpayTimestamp, wechatpayNonce, body)
.collect(Collectors.joining("\n", "", "\n"));
}
public AutoUpdateCertificatesVerifier getVerifier() {
AutoUpdateCertificatesVerifier verifier = (AutoUpdateCertificatesVerifier) VERIFIER_MAP.get("verifier");
Date notAfter = (Date) VERIFIER_MAP.get("notAfter");
if(null == verifier || null == notAfter || notAfter.before(new Date())){
// 加载证书
KeyPair keyPair = createPKCS12("/apiclient_cert.p12",
"Tenpay Certificate", MCHID);
// 加载官方自动更新证书
verifier = new AutoUpdateCertificatesVerifier(new WechatPay2Credentials(MCHID,
new PrivateKeySigner(CERTNO, keyPair.getPrivate())),
V3KEY.getBytes(StandardCharsets.UTF_8));
VERIFIER_MAP.put("verifier", verifier);
VERIFIER_MAP.put("notAfter", verifier.getValidCertificate().getNotAfter());
}
return verifier;
}
}
官方证书验证工具放到了全局变量中,
4.支付下单实现
@ApiOperation(value = "支付", httpMethod = "POST")
@RequestMapping(value = "/api/pay/order", method = RequestMethod.POST)
@Transactional
public PrePayResponse wxToPay(@Valid @RequestBody PayOrderRequest dto, HttpServletRequest request, HttpServletResponse response) throws Exception {
PrePayResponse res = new PrePayResponse();
// 用户登录校验
......
// 获取用户(数据库中的实体类)
WechatUser user = xxx.getUser();
int fee = 0;
// 得到小程序传过来的价格,注意这里的价格必须为整数,1代表1分,所以传过来的值必须*100;
if (null != dto.getPrice()) {
double price = dto.getPrice() * 100;
fee = (int) price;
}
// 生成订单号,商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
String OutTradeNo = DateUtil.getOrderNum() + String.valueOf(System.currentTimeMillis()).substring(4) + new Random().nextInt(999999999);
// 保存订单信息
PayOrder order = new PayOrder();
order.setUser(user);
order.setCreatedAt(new Date());
order.setPrice(dto.getPrice());
order.setTradeNo(OutTradeNo);
......
payOrderRepository.save(order);
// 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
String basePath = request.getScheme() + "://" + request.getServerName() + ":" +
request.getServerPort() + request.getContextPath();
String notify_url = basePath+"/api/anon/pay/notify";
//请求参数
cn.hutool.json.JSONObject json = new cn.hutool.json.JSONObject();
json.set("appid", APPID);
json.set("mchid", MCHID); // 商户号
json.set("description", "test"); // 商品描述,必填
json.set("out_trade_no", OutTradeNo);
json.set("attach", "自定义参数"); // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
json.set("notify_url", notify_url); // 回调地址
cn.hutool.json.JSONObject amount = new cn.hutool.json.JSONObject();
amount.set("total", fee);
amount.set("currency", "CNY");
json.set("amount",amount); // 订单金额
cn.hutool.json.JSONObject payer = new cn.hutool.json.JSONObject();
payer.set("openid", user.getOpenId()); // 用户openId
json.set("payer",payer); // 支付者
//获取token
String nonceStr = keyPairFactory.generateNonceStr();
long timestamp = System.currentTimeMillis()/1000;
res.setNonceStr(nonceStr);
res.setTimeStamp(timestamp);
String wechatToken = this.getToken(JSON.toJSONString(json), "POST", "/v3/pay/transactions/jsapi", nonceStr, timestamp);
Map<String,String> headers = new HashMap<String,String>();
headers.put("Accept","application/json");
headers.put("Content-Type","application/json; charset=utf-8");
headers.put("Authorization",wechatToken);
HttpResponse httpResponse = HttpRequest.post("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi")
.headerMap(headers, false)
.body(String.valueOf(json))
.timeout(5 * 60 * 1000)
.execute();
String resultBody = httpResponse.body();
com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(resultBody);
String prepayId = jsonObject.getString("prepay_id");
if(StringUtils.isEmpty(prepayId)){
order.setStatus(-1);
res.setCode(Constants.SC_MSG);
res.setMessage("支付失败");
return res;
}
res.setPrepayId(prepayId);
res.setSignType("RSA");
// 加载证书
KeyPair keyPair = keyPairFactory.createPKCS12("/apiclient_cert.p12",
"Tenpay Certificate", MCHID);
String paySign = keyPairFactory.awakenPaySign(APPID, timestamp, nonceStr, "prepay_id="+prepayId, keyPair);
res.setPaySign(paySign);
res.setCode(Constants.SC_OK);
return res;
}
private String getToken(String body,String method,String url, String nonceStr, Long timestamp) {
//1.加载证书
KeyPair keyPair = keyPairFactory.createPKCS12("/apiclient_cert.p12", "Tenpay Certificate", MCHID);
//2.获取签名
String sign = keyPairFactory.requestSign(method, url, timestamp, nonceStr, body,keyPair);
//3.封装token
String token = keyPairFactory.token(MCHID, nonceStr, timestamp, CERTNO, sign);
return token;
}
5.支付回调
@ApiOperation(value = "支付回调地址")
@RequestMapping(value = "/api/anon/pay/notify", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
@Transactional
public ResultResponse notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
ResultResponse res = new ResultResponse();
// 从请求头获取验签字段
String Timestamp = request.getHeader("Wechatpay-Timestamp");
String Nonce = request.getHeader("Wechatpay-Nonce");
String Signature = request.getHeader("Wechatpay-Signature");
String Serial = request.getHeader("Wechatpay-Serial");
// 获取官方验签工具
AutoUpdateCertificatesVerifier verifier = keyPairFactory.getVerifier();
// 读取请求体的信息
ServletInputStream inputStream = request.getInputStream();
StringBuffer stringBuffer = new StringBuffer();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String s;
// 读取回调请求体
while ((s = bufferedReader.readLine()) != null) {
stringBuffer.append(s);
}
String s1 = stringBuffer.toString();
Map requestMap = (Map) JSON.parse(s1);
String resource = String.valueOf(requestMap.get("resource"));
Map requestMap2 = (Map) JSON.parse(resource);
String associated_data = requestMap2.get("associated_data").toString();
String nonce = requestMap2.get("nonce").toString();
String ciphertext = requestMap2.get("ciphertext").toString();
//按照文档要求拼接验签串
String VerifySignature = Timestamp + "\n" + Nonce + "\n" + s1 + "\n";
// System.out.println("拼接后的验签串=" + VerifySignature);
//使用官方验签工具进行验签
boolean verify = verifier.verify(Serial, VerifySignature.getBytes(), Signature);
//判断验签的结果
// System.out.println("验签结果:"+verify);
// 验签成功
// System.out.println("验签成功后,开始进行解密");
com.wechat.pay.contrib.apache.httpclient.util.AesUtil aesUtil = new AesUtil(V3KEY.getBytes());
String aes = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(aes);
if ("SUCCESS".equals(jsonObject.getString("trade_state"))) {
String out_trade_no = jsonObject.getString("out_trade_no");
// 订单不为空
if (!StringUtils.isEmpty(out_trade_no)) {
//支付成功后的业务处理
PayOrder payOrder = payOrderRepository.findByTradeNo(out_trade_no);
payOrder.setStatus(1);
......
res.setCode(Constants.SC_OK);
return res;
}
} else {
// 支付失败后的操作
}
res.setCode(Constants.SC_OK);
return res;
}
注意:回调接口不要被拦截