注意: 个人无法实现微信支付,需是商家或企业
微信文档写的比较乱:
看文档顺序:
1.开发指引
2.JSAPI下单 (因为小程序调用的是JSAPI支付类型)
3.接口规则
左边下拉框的 证书/密钥/签名介绍 可以看一下
一篇特别好的文章 : 公钥,私钥和数字签名这样最好理解
5.SDK
这里使用的是这个 wechatpay-apache-httpclient 工具包
使用它构造HttpClient。得到的HttpClient在执行请求时将自动携带身份认证信息,并检查应答的微信支付签名。且此SDK还能定时更新平台证书
下面maven导入的也是其依赖包
6.查询订单
7.小程序调起支付API
8.支付通知
小程序微信支付流程图
由上图可知需写三个API
1.获取支付信息
前端请求下单支付,后端返回支付参数
详情见: JSAPI下单
2.查询订单
通过微信支付订单号或商户订单号获取其支付数据
详情见: 查询订单API
微信支付商户号:注册公众号时有的,唯一的
微信支付订单号:微信自动生成的订单号
商户订单号:自己生成的订单号
3.支付通知API
注: yaml文件里配置的 notify-url 为自己写的支付通知API的 @PostMapping 里的地址
详情见: 小程序调起支付API
签名和各类数据的加密解密是微信支付的难点
code
用到的依赖
<!--微信支付-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.2</version>
</dependency>
<!--Hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
配置文件 .yaml
多加如下配置
wechat-payment:
#微信支付商户号
merchant-id: "xxxxxx"
#微信支付证书序列号
cert-serial-number: "xxxxxx"
#微信支付API V3秘钥
api-v3-key: "xxxxxx"
#微信支付小程序APP ID
app-id: "xxxxxx"
#我的证书放在resources下wechat-payment-cert文件夹里
#微信支付公钥证书位置
public-cert-location: "/wechat-payment-cert/apiclient_cert.pem"
#微信支付私钥证书位置
private-key-location: "/wechat-payment-cert/apiclient_key.pem"
#支付回调地址
notify-url: "http://127.0.0.1/wechat-payment/paymentNotify"
调用到的工具类:
用来对签名和数据进行加密解密
import org.apache.commons.codec.binary.Base64;
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.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.*;
public class SignUtil {
//编码方式
private static final String ENCODING = "UTF-8";
//加密方式
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
//加密
//调用顺序:先加密后编码:先 sign256 后 encodeBase64
public static byte[] sign256(String data, PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeyException,
SignatureException, UnsupportedEncodingException {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data.getBytes(ENCODING));
return signature.sign();
}
//验证签名正确与否
//调用顺序:先解码后验证:先decodeBase64后verify256
public static boolean verify256(String data, byte[] sign, PublicKey publicKey) {
if (data == null || sign == null || publicKey == null) {
return false;
}
try {
Signature signCheck = Signature.getInstance(SIGNATURE_ALGORITHM);
signCheck.initVerify(publicKey);
signCheck.update(data.getBytes(StandardCharsets.UTF_8));
return signCheck.verify(sign);
} catch (Exception e) {
return false;
}
}
//编码
public static String encodeBase64(byte[] bytes) {
return new String(Base64.encodeBase64(bytes));
}
//解码
public static byte[] decodeBase64(String data) {
byte[] result = null;
try {
result = Base64.decodeBase64(data);
} catch (Exception e) {
return null;
}
return result;
}
/**
* 解密请求回调
* 参考https://developers.weixin.qq.com/community/develop/article/doc/000eeac2ba4898a9fd6be8b175bc13
*
* @param apiV3Key 微信API V3 秘钥
* @param associatedData 附加数据
* @param nonce 随机串
* @param ciphertext 数据密文
* @return 解密后的数据密文
*/
public static String decryptWechatPaymentResponseBody(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);
}
}
}
用到的实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeChatPaymentOrder {
/**
* 商品描述 string[1,127]
*/
String description;
/**
* 商户订单号 string[6,32]
*/
String outTradeNumber;
/**
* 商户订单号 string[6,32]
* 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
*/
String notifyUrl;
/**
* 总金额,单位为分
*/
Integer totalAmount;
/**
* 支付者
*/
String userOpenId;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WechatPaymentData {
/**
* 时间戳
* 标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。
*/
String timeStamp;
/**
* 随机字符串
*/
String nonceStr;
/**
* 订单详情扩展字符串
* 小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
*/
String orderPackage;
/**
* 签名方式
* 签名类型,默认为RSA,仅支持RSA。
*/
String signType;
/**
* 签名
* 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
*/
String paySign;
}
controller层
@Slf4j
@CrossOrigin
@RestController
public class WechatPaymentController {
@Resource
private WechatPaymentService wechatPaymentService;
@Resource
private WechatUserService wechatUserService;
/**
* 1.获取支付信息
*
* @param loginCode 登录Code(与userOpenId二选一)
* @param userOpenId 用户的微信OpenId (与loginCode二选一)
* @param description 商品描述
* @param totalAmount 总价
* @return 支付数据(返回支付参数)
*/
@PostMapping("/wechat-payment/getWechatPaymentData")
public WechatPaymentData getWechatPaymentData(String loginCode,
String userOpenId,
@RequestParam String description,
@RequestParam Integer totalAmount) {
//验证描述长度
int descriptionLength = description.length();
if (descriptionLength < 1 || descriptionLength > 127) {
log.warn("description字段长度限制为[1,126]");
return null;
}
//如果是loginCode的话就需要转化为OpenId
if (loginCode != null) {
userOpenId = wechatUserService.getUserWechatOpenId(loginCode);
}
//如果为空则不继续执行了
if (userOpenId == null) {
log.warn("无法获取用户标识符");
return null;
} else {
log.info("已获取用户OpenId:" + userOpenId);
}
return wechatPaymentService.getWechatPaymentData(userOpenId, description, totalAmount);
}
/**
* 2.查询支付记录
*
* @param transactionId 微信支付订单号(与商户订单号二选一)
* @param outTradeNumber 商户订单号(与微信支付订单号二选一)
* @return 支付数据
*/
@GetMapping("/wechat-payment/getTransaction")
public JSONObject getTransaction(String transactionId, String outTradeNumber) {
if (transactionId == null && outTradeNumber == null) {
log.warn("参数错误");
return null;
}
return wechatPaymentService.getTransaction(transactionId, outTradeNumber);
}
/**
* 3.微信支付回调
*
* @param body 微信提供的支付回调数据
*/
@PostMapping("/wechat-payment/paymentNotify")
public void paymentNotify(
@RequestHeader Map<String, String> headers,
@RequestBody String body) {
//验证签名
boolean signResult = wechatPaymentService.verifyHttpSign(headers, body);
if (!signResult) {
log.warn("签名验证失败");
return;
}
//数据处理
JSONObject notifyObj = JSON.parseObject(body);
wechatPaymentService.paymentNotify(notifyObj);
//通过body数据判断是否支付成功
//将paymentNotify方法改为JSONObject返回值,返回数据处理(解密)过后的notifyObj
//做支付成功判断并插入数据库
// ...
}
}
service层:
@Slf4j
@Service
public class WechatPaymentService {
/**
* 微信支付商户号
*/
@Value("${wechat-payment.merchant-id}")
String paymentMerchantId;
/**
* 微信支付API V3秘钥
*/
@Value("${wechat-payment.api-v3-key}")
String paymentApiV3Key;
/**
* 微信支付小程序APP ID
*/
@Value("${wechat-payment.app-id}")
String paymentAppId;
/**
* 支付回调地址
*/
@Value("${wechat-payment.notify-url}")
String paymentNotifyUrl;
/**
* 验证器
*/
@Resource
Verifier merchantVerifier;
/**
* 私钥
*/
@Resource
PrivateKey merchantPrivateKey;
/**
* Http客户端
*/
@Resource
CloseableHttpClient httpClient;
/**
* 验证签名请求头
*
* @param headers HTTP请求头
* @param responseBody 应答主体
* @return 是否通过验证
*/
public Boolean verifyHttpSign(Map<String, String> headers, String responseBody) {
/*
* 注意,所有的HTTP请求的header都是小写字母
* 微信文档中给的不对
* */
log.info("获取到的平台Header");
log.info(String.valueOf(headers));
//获取可用的微信平台证书
X509Certificate wechatCertificate = merchantVerifier.getValidCertificate();
//获取微信平台证书的序列号(16进制转换)
String wechatCertificateSerialNumber = wechatCertificate.getSerialNumber().toString(16);
//获取传递的证书序列号
String certSerial = headers.get("wechatpay-serial");
//验证证书序列号(忽略大小写)
if (!certSerial.equalsIgnoreCase(wechatCertificateSerialNumber)) {
log.error("证书序列号不一致");
return false;
}
//应答时间戳(这里一定要写)
String timeStamp = headers.get("wechatpay-timestamp");
//应答随机串
String nonce = headers.get("wechatpay-nonce");
//构建应答的验签名串
String signText = timeStamp + "\n" + nonce + "\n" + responseBody + "\n";
log.info("应答的验签名串");
log.info(signText);
//微信支付的应答签名(BASE64加密后)
String base64Signature = headers.get("wechatpay-signature");
//获取签名
byte[] sign = SignUtil.decodeBase64(base64Signature);
//验证
return SignUtil.verify256(signText, sign, wechatCertificate.getPublicKey());
}
/**
* 查询支付记录
*
* @param transactionId 微信支付订单号
* @param outTradeNumber 商户订单号
* @return 支付记录
*/
public JSONObject getTransaction(String transactionId, String outTradeNumber) {
String url;
if (transactionId != null) {
//微信支付订单号模式
url = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/transaction_id?mchid=merchantId";
url = url.replace("transaction_id", transactionId);
//微信支付商户号
url = url.replace("merchantId", paymentMerchantId);
} else if (outTradeNumber != null) {
//商户订单号模式
url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/out_trade_no?mchid=merchantId";
url = url.replace("out_trade_no", outTradeNumber);
url = url.replace("merchantId", paymentMerchantId);
} else {
log.error("错误的数据请求");
return null;
}
//组装请求参数
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Accept", "application/json");
httpGet.addHeader("Content-type", "application/json; charset=utf-8");
//发送数据
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
} catch (IOException e) {
e.printStackTrace();
}
//解析数据
try {
if (response != null) {
String bodyAsString = EntityUtils.toString(response.getEntity());
return JSON.parseObject(bodyAsString);
} else {
log.error("数据为空");
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 处理支付回调
*
* @param jsonObject 数据
*/
public void paymentNotify(JSONObject jsonObject) {
//获取通知数据
JSONObject resource = jsonObject.getJSONObject("resource");
//附加数据
String associatedData = resource.getString("associated_data");
//随机串
String nonce = resource.getString("nonce");
//数据密文
String ciphertext = resource.getString("ciphertext");
//获得解密数据
String decryptString = SignUtil.decryptWechatPaymentResponseBody(paymentApiV3Key, associatedData, nonce, ciphertext);
//解析数据
JSONObject decryptData = JSONObject.parseObject(decryptString);
log.info("获得解析数据:");
log.info(String.valueOf(decryptData));
}
/**
* 获取微信支付数据
*
* @param userOpenId 用户微信OpenId
* @param description 描述
* @param totalAmount 总价
* @return 支付数据(返回支付参数)
*/
public WechatPaymentData getWechatPaymentData(String userOpenId, String description, Integer totalAmount) {
//生成订单号
String outTradeNumber = IdUtil.simpleUUID();
//组装订单数据:商品描述、商户订单号、商户订单号、支付金额、支付者
WeChatPaymentOrder paymentOrder = new WeChatPaymentOrder(
description,
outTradeNumber,
paymentNotifyUrl,
totalAmount,
userOpenId
);
//获取PrePayId
String prePayID = generatePrePayId(paymentOrder);
if (prePayID == null) {
log.error("无法获取PrePayID");
return null;
}
//获取当前时间戳(微信要求10位)
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
//订单详情扩展字符串
String orderPackage = "prepay_id=" + prePayID;
//签名方式
String signType = "RSA";
//待签名字符串
String paySignText = paymentAppId + "\n" + timeStamp + "\n" + outTradeNumber + "\n" + orderPackage + "\n";
log.info("待签名字符串:");
log.info(paySignText);
//签名结果
String signResult;
try {
//获取加密后的签名/对签名进行加密
byte[] sign256 = SignUtil.sign256(paySignText, merchantPrivateKey);
signResult = SignUtil.encodeBase64(sign256);
} catch (Exception e) {
e.printStackTrace();
return null;
}
log.info("签名结果:");
log.info(signResult);
return new WechatPaymentData(
timeStamp,
outTradeNumber,
orderPackage,
signType,
signResult
);
}
/**
* 生成PrePayId
*
* @param data 支付数据
* @return 获取PrePayId
*/
private String generatePrePayId(WeChatPaymentOrder data) {
try {
//组装数据
JSONObject rootNode = new JSONObject();
//应用ID
rootNode.put("appid", paymentAppId);
//直连商户号
rootNode.put("mchid", paymentMerchantId);
//商品描述 string[1,127]
rootNode.put("description", data.getDescription());
//商户订单号 string[6,32]
//商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
rootNode.put("out_trade_no", data.getOutTradeNumber());
//通知地址
rootNode.put("notify_url", data.getNotifyUrl());
//订单金额
JSONObject amountObject = new JSONObject();
amountObject.put("total", data.getTotalAmount());
rootNode.put("amount", amountObject);
//支付者
JSONObject payerObject = new JSONObject();
payerObject.put("openid", data.getUserOpenId());
rootNode.put("payer", payerObject);
//组装请求参数
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
httpPost.setEntity(new StringEntity(rootNode.toString(), "UTF-8"));
//发送数据
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析数据
String bodyAsString = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(bodyAsString);
String prePayId = jsonObject.getString("prepay_id");
if (prePayId != null) {
log.info("已生成PrePayId:" + prePayId);
return prePayId;
} else {
log.error("无法获取PrePayId,错误信息:" + bodyAsString);
return null;
}
} catch (Exception exception) {
exception.printStackTrace();
return null;
}
}
}