微信支付学习笔记
- 一、获取相关信息
- 二、API V3
- 2.1、引依赖
- 2.2、写配置类
- 2.2.1、获取商户的私钥文件
- 2.2.2、获取签名验证器
- 2.2.3、获取http请求对象
- 2.3、下单显示微信支付二维码
- 2.4、微信返回用户支付信息
- 2.5、解密微信返回信息用于更新订单
- 2.6、用户取消订单
一、获取相关信息
获取:标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL
- 获取商户号
微信商户平台:https://pay.weixin.qq.com/ 步骤:申请成为商户 => 提交资料 => 签署协议 => 获取商户号 - 获取AppID
微信公众平台:https://mp.weixin.qq.com/ 步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号 - 申请商户证书
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥 - 获取微信的证书
可以预先下载,也可以通过编程的方式获取。 - 获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥
二、API V3
2.1、引依赖
<!--wechatpay-sdk-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.3.0</version>
</dependency>
2.2、写配置类
2.2.1、获取商户的私钥文件
将获取的私钥文件apiclient_key.pem放入resources下
private PrivateKey getPrivateKey(String filename){
try {
InputStream systemResourceAsStream = this.getClass().getResourceAsStream("/apiclient_key.pem");
return PemUtil.loadPrivateKey(systemResourceAsStream);
} catch (Exception e) {
throw new RuntimeException("私钥文件不存在", e);
}
}
进行测试时先将配置类改成public 再在测试类里加上引入的@Resource
如果输出的是一条地址,需要换高版本的JDK
@GetMapping("/getsiyao")
public void getsiyao(){
PrivateKey privateKey = wxPayConfig.getPrivateKey("");
System.out.println(privateKey);
}
2.2.2、获取签名验证器
@Bean
public ScheduledUpdateCertificatesVerifier getVerifier(){
System.out.println("获取签名验证器");
//获取商户私钥
PrivateKey privateKey = getPrivateKey("");
//私钥签名对象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
//身份认证对象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定时更新的签名验证器,不需要传入证书
ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
wechatPay2Credentials,
apiV3Key.getBytes(StandardCharsets.UTF_8));
return verifier;
}
2.2.3、获取http请求对象
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
System.out.println("获取httpClient");
//获取商户私钥
PrivateKey privateKey = getPrivateKey("");
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
CloseableHttpClient httpClient = builder.build();
return httpClient;
}
2.3、下单显示微信支付二维码
service层
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
//TODO:生成订单...
log.info("调用统一下单API");
//调用统一下单API
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 请求body参数
Map paramsMap = new HashMap();
//设置参数(不全 看官方文档)...
paramsMap.put("appid", wxPayConfig.getAppid());
paramsMap.put("mchid", wxPayConfig.getMchId());
paramsMap.put("notify_url", "填支付信息的返回地址");
//将参数转换成json字符串
String jsonParams =JSON.toJSONString(paramsMap);
log.info("请求参数 ===> {}" + jsonParams);
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功, 返回结果 = " + bodyAsString);
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功");
} else {
log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
throw new IOException("request failed");
}
//响应结果
Map<String, String> resultMap = JSON.parseObject(bodyAsString,HashMap.class);
//二维码
String codeUrl = resultMap.get("code_url");
//TODO:保存二维码
//返回二维码
Map<String, Object> map = new HashMap<>();
map.put("codeUrl", codeUrl);
map.put("orderNo", orderInfo.getOrderNo());
return map;
} finally {
response.close();
}
}
2.4、微信返回用户支付信息
controller层
/**
* 支付通知
* 微信支付通过支付通知接口将用户支付成功消息通知给商户
*/
@ApiOperation("支付通知")
@PostMapping("/native/notify")
public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
Map<String, String> map = new HashMap<>();//应答对象
try {
//处理通知参数
String body = HttpUtils.readData(request);//处理微信发回来的信息
Map<String, Object> bodyMap = JSON.parseObject(body, HashMap.class);
String requestId = (String)bodyMap.get("id");
log.info("支付通知的id ===> {}", requestId);
//log.info("支付通知的完整数据 ===> {}", body);
//签名的验证
WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
= new WechatPay2ValidatorForRequest(verifier, requestId, body);
if(!wechatPay2ValidatorForRequest.validate(request)){
log.error("通知验签失败");
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "通知验签失败");
return JSON.toJSONString(map);
}
log.info("通知验签成功");
//TODO:处理订单
log.info("处理订单成功");
//应答超时
//模拟接收微信端的重复通知
//TimeUnit.SECONDS.sleep(5);
//成功应答
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "成功");
return JSON.toJSONString(map);
} catch (Exception e) {
e.printStackTrace();
//失败应答
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "失败");
return JSON.toJSONString(map);
}
}
微信SDK里只有对回应验签,没有对请求验签,仿造SDK自己写一个
utils层
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
* @author xy-peng
*/
public class WechatPay2ValidatorForRequest {
protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
/**
* 应答超时时间,单位为分钟
*/
protected static final long RESPONSE_EXPIRED_MINUTES = 5;
protected final Verifier verifier;
protected final String requestId;
protected final String body;
public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) throws IOException {
try {
//处理请求参数
validateParameters(request);
//构造验签名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//验签
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判断请求是否过期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期请求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(HttpServletRequest request) throws IOException {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
2.5、解密微信返回信息用于更新订单
微信返回信息进行了对称加密需要解密出ciphertext才行
service层
@Transactional(rollbackFor = Exception.class)
@Override
public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("处理订单");
//解密报文
String plainText = decryptFromResource(bodyMap);
//将明文转换成map
HashMap plainTextMap = JSON.parseObject(plainText, HashMap.class);
String orderNo = (String)plainTextMap.get("out_trade_no");
/*在对业务数据进行状态检查和处理之前,
要采用数据锁进行并发控制,
以避免函数重入造成的数据混乱*/
//尝试获取锁:
//成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
if(lock.tryLock()){
try {
//处理重复的通知
//接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
//TODO:根据订单号查询订单状态,如果是支付成功状态直接返回空...
// //模拟通知并发
// try {
// TimeUnit.SECONDS.sleep(5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//TODO:更新订单状态
//TODO:记录支付日志
} finally {
//要主动释放锁
lock.unlock();
}
}
}
解密部分代码:
service层
/**
* 对称解密
* @param bodyMap
* @return
*/
private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
log.info("密文解密");
//通知数据
Map<String, String> resourceMap = (Map) bodyMap.get("resource");
//数据密文
String ciphertext = resourceMap.get("ciphertext");
//随机串
String nonce = resourceMap.get("nonce");
//附加数据
String associatedData = resourceMap.get("associated_data");
log.info("密文 ===> {}", ciphertext);
AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info("明文 ===> {}", plainText);
return plainText;
}
2.6、用户取消订单
controller
/**
* 用户取消订单
* @param orderNo
* @return
* @throws Exception
*/
@ApiOperation("用户取消订单")
@PostMapping("/cancel/{orderNo}")
public R cancel(@PathVariable String orderNo) throws Exception {
log.info("取消订单");
wxPayService.cancelOrder(orderNo);
return R.ok().setMessage("订单已取消");
}
Service
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) throws Exception {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//TODO:更新商户端的订单状态...
}
/**
* 用户取消订单
* @param orderNo
*/
@Override
public void cancelOrder(String orderNo) throws Exception {
//调用微信支付的关单接口
this.closeOrder(orderNo);
//TODO:更新商户端的订单状态...
}
/**
* 关单接口的调用
* @param orderNo
*/
private void closeOrder(String orderNo) throws Exception {
log.info("关单接口的调用,订单号 ===> {}", orderNo);
//创建远程请求对象
String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
url = wxPayConfig.getDomain().concat(url);
HttpPost httpPost = new HttpPost(url);
//组装json请求体
Map<String, String> paramsMap = new HashMap<>();
paramsMap.put("mchid", wxPayConfig.getMchId());
String jsonParams=JSON.toJSONString(paramsMap);
log.info("请求参数 ===> {}", jsonParams);
//将请求参数设置到请求对象中
StringEntity entity = new StringEntity(jsonParams,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//完成签名并执行请求
CloseableHttpResponse response = wxPayClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();//响应状态码
if (statusCode == 200) { //处理成功
log.info("成功200");
} else if (statusCode == 204) { //处理成功,无返回Body
log.info("成功204");
} else {
log.info("Native下单失败,响应码 = " + statusCode);
throw new IOException("request failed");
}
} finally {
response.close();
}
}