springboot整合微信支付V3使用JSAPI下单全流程,无调用第三方封装易读(完整源码)可开箱享用
文章目录
- springboot整合微信支付V3使用JSAPI下单全流程,无调用第三方封装易读(完整源码)可开箱享用
- 前言
- 一.直接上流程图
- 二.话不多说直接开写,具体第一步的准备工作准备完整全部信息参数会在后续做出说明
- 1.创建springboot工程引入如下依赖
- 2.在项目resource下创建wxpay_v3.properties文件配置V3支付参数
- 3.编写文件读取上述配置自己也可放在YML进行读取,创建WxPayConfig.class配置类
- 4.编写constants常量类声明微信相关接口地址
- 5.根据微信官方说明文档书写解密工具类
- 6.编写JSON转换的类提供调用接口返回参数的转换
- 7.编写订单实体
- 8.编写service 声明接口
- 9.编写实现类具体的调用实现,由于我的config读取配置文件使用注入的方式所以,加密解密什么的都写在实现类这样可以直接使用注入,并没有提出来封装到util但是注释清楚易懂
- 10.编写controller暴露接口
前言
目前由于项目需要搞了一个微信支付功能是用于微信小程序端的,时间比较赶看了很久微信官方文档和很多百度的博客一头雾水,怎么搞从哪里搞无头绪! 要么是微信文档太官方各种加密解密太繁琐,要么是像IJPay等优秀开源项目封装太厉害不知如何排查问题!故此抓掉头发总结各大博主经验和微信官方文档,呕心沥血搞出如下适用于微信V3支付JSAPI下单的代码,可供各路大佬享用!注释完整,源码完整,张贴即可上车;
提示:本文主要实现的是V3版本JSAPI下单,小程序端使用!小程序端使用! 其他APP或者native扫码付请另请高明
一.直接上流程图
1.申请小程序或公众号,注册称为商户获取支付参数步骤
2.调用JSAPI调起微信支付流程
二.话不多说直接开写,具体第一步的准备工作准备完整全部信息参数会在后续做出说明
1.创建springboot工程引入如下依赖
<!--json解析-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!--http依赖发送请求-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.2</version>
</dependency>
<!-- 糊涂工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.2</version>
</dependency>
<!--lombok 减少getset-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--xml 读取工具-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-avro</artifactId>
</dependency>
2.在项目resource下创建wxpay_v3.properties文件配置V3支付参数
#appid 支付商户关联的 小程序ID或公众号ID(登陆微信公众平台–>开发–>基本配置–>开发者ID(AppID))
v3.appId=wx172hvs19744c8653
# 对应apiclient_key.pem所在路径(商户平台->账户中心->证书->下载后一共3个:主要为apiclient_key.pem 这个) 将该文件开发时放在项目目录,上线放在项目根目录单独放不要一起打包
v3.keyPath=/Users/apiclient_key.pem
v3.certPath=/Users/apiclient_cert.pem
#微信支付商户号(登陆商户平台–>账户中心–>商户信息–>微信支付商户号)
v3.mchId=1234399709
#V3秘钥(商户平台->账户中心->V3秘钥->设置32位)
v3.v3Key=14eoil2d39vs452084b1c3f3c3131339
#证书序列号(这个是根据key文件和其他商户参数调微信支付接口返回具体代码有,先调用一次进行写入保存)
v3.mchSerialNo=48D429FF87CC967B09EB6219C9382A0CC4A79D66
#下单成功微信通知URL即微信回调URL,只支持https域名,所以开发时可以使用花生壳等进行映射
v3.notifyOrderUrl=https://youdomain.com/app/pay/callback
#退款URL如上一步自行配置
v3.notifyRefoundUrl=https://youdomian.com
#退款回调URL 如上一步自行配置
v3.returnUrl=https://youdomian.com
#小程序或公众号秘钥,(登陆微信公众平台–>开发–>基本配置–>secret)
v3.secret=t23eb5fae1cb58a1ff4d00987ee12344
温馨提示:如上信息均做脱敏需要按照自己的进行修改
3.编写文件读取上述配置自己也可放在YML进行读取,创建WxPayConfig.class配置类
package com.matinzac.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* <p>Description: 封装读取微信支付配置信息的配置类</p>
*
* @author MtinZac
* create on 2021/11/26 上午10:30
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
@PropertySource("classpath:/wxpay_v3.properties")
@ConfigurationProperties(prefix = "v3")
public class WxPayConfig {
/**
* 小程序ID
*/
private String appId;
/**
* client_key 路径
*/
private String keyPath;
/**
* cert 路径
*/
private String certPath;
/**
* 商户ID
*/
private String mchId;
/**
* V3 key秘钥
*/
private String v3Key;
/**
* API证书序列号
*/
private String mchSerialNo;
/**
* 支付成功回调URL
*/
private String notifyOrderUrl;
/**
* 退款回调路径
*/
private String notifyRefoundUrl;
/**
* 退款成功返回路径
*/
private String returnUrl;
/**
* 开发者秘钥
*/
private String secret;
/**
* // 定义全局容器 保存微信平台证书公钥
*/
public Map<String, X509Certificate> certificateMap = new ConcurrentHashMap<>();
}
4.编写constants常量类声明微信相关接口地址
package com.matinzac.constants;
/**
* <p>Description: 微信支付相关 URL 通用类</p>
*
* @author MtinZac
* create on 2021/11/24 上午11:14
*/
public class WxPayConstants {
/**
* 微信code换openid 和 unionid 的URL
*/
public static final String COUDE_OPENID_URL ="https://api.weixin.qq.com/sns/jscode2session?appid=";
/**
* 适用对象:小程序下单
* 请求方式:POST
*/
public static final String JSAPIURL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
/**
* 适用对象:APP下单
* 请求方式:POST
*/
public static final String APPURL = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";
/**
* 获取证书
*/
public static final String CERTIFICATESURL = "https://api.mch.weixin.qq.com/v3/certificates";
/**
* 退款地址
*/
public static final String REFUNDSURL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
}
5.根据微信官方说明文档书写解密工具类
package com.matinzac.util;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* <p>Description:解密工具类用于微信请求和返回信息解密</p>
*
* @author MtinZac
* create on 2021/11/26 上午10:40
*/
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "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)), "utf-8");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
6.编写JSON转换的类提供调用接口返回参数的转换
package com.sbm.common.utils.pay.json;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
import org.codehaus.jackson.type.JavaType;
import org.codehaus.jackson.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
/**
* <p>Description: 微信支付JSON转换类</p>
*
* @author MatinZac
* create on 2021/11/24 上午11:15
*/
public class JsonUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
private static Logger log = LoggerFactory.getLogger(JsonUtil.class);
static {
// 对象的所有字段全部列入
objectMapper.setSerializationInclusion(Inclusion.ALWAYS);
// 取消默认转换timestamps形式
objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
// 忽略空Bean转json的错误
objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);
// 所有的日期格式都统一为以下的样式,即yyyy-MM-dd HH:mm:ss
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 精度的转换问题
objectMapper.configure(DeserializationConfig.Feature.USE_BIG_DECIMAL_FOR_FLOATS, true);
objectMapper.configure(DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
public static <T> String obj2String(T obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.warn("Parse Object to String error", e);
return null;
}
}
public static <T> String obj2StringPretty(T obj) {
if (obj == null) {
return null;
}
try {
return obj instanceof String ? (String) obj
: objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (Exception e) {
log.warn("Parse Object to String error", e);
return null;
}
}
public static <T> T string2Obj(String str, Class<T> clazz) {
if (StringUtils.isEmpty(str) || clazz == null) {
return null;
}
try {
return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
} catch (Exception e) {
log.warn("Parse String to Object error", e);
return null;
}
}
public static <T> T string2Obj(String str, TypeReference<T> typeReference) {
if (StringUtils.isEmpty(str) || typeReference == null) {
return null;
}
try {
return (T) (typeReference.getType().equals(String.class) ? str
: objectMapper.readValue(str, typeReference));
} catch (Exception e) {
log.warn("Parse String to Object error", e);
return null;
}
}
public static <T> T string2Obj(String str, Class<?> collectionClass, Class<?>... elementClasses) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
try {
return objectMapper.readValue(str, javaType);
} catch (Exception e) {
log.warn("Parse String to Object error", e);
return null;
}
}
}
7.编写订单实体
package com.matinzac.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* <p>Description: 支付订单信息</p>
*
* @author MtinZac
* create on 2021/11/26 上午10:30
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayOrder {
/**
* 表主键ID
*/
private Integer id;
/**
* 订单编号
*/
private String outTradeNo;
/**
* 用户客户端ID
*/
private String openId;
/**
* 支付总金额
*/
private String totalFee;
/**
* 支付货币类型(CNY人民币)
*/
private String payerCurrency;
/**
* 商品描述信息
*/
private String remarkBody;
/**
* 支付状态:0未支付;1支付失败;2支付成功
*/
private String payStatus;
/**
* 订单创建时间
*/
private Date orderCreateTime;
/**
* 支付成功时间
*/
private Date paySuccessTime;
/**
* 支付失败时间
*/
private Date payErrorTime;
/**
* 支付失败原因
*/
private String payErrorReason;
/**
* 修改时间
*/
private Date updateTime;
}
8.编写service 声明接口
package com.matinzac.service;
import com.matinzac.entity.PayOrder;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* <p>Description: 微信支付相关接口</p>
*
* @author MtinZac
* create on 2021/11/26 上午10:40
*/
public interface WxPayService {
/**
* 获取商户证书编号
* @return 商户证书编号
*/
String getMchSerialNo();
/**
* 通过code 获取 oppenId 接口
* @return 从微信返回截取的 openid
*/
String getOpenId(String code);
/**
* 通过oppenId 以及支付相关信息进行请求支付参数返回
* @param openid 客户端ID
* @param time_expire 订单过期时间
* @param order 订单信息
* @return
*/
Map<String, Object> getPayInfo(String openid,String time_expire, PayOrder order);
/**
* 支付成功回调接口
* @param body 微信支付返回参数信息
* @param request 请求体
* @return 解析返回参数进行反馈
*/
Map<String, Object> callBack(Map body, HttpServletRequest request);
}
9.编写实现类具体的调用实现,由于我的config读取配置文件使用注入的方式所以,加密解密什么的都写在实现类这样可以直接使用注入,并没有提出来封装到util但是注释清楚易懂
package com.matinzac.service.impl;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.matinzac.config.WxPayConfig;
import com.matinzac.constants.WxPayConstants;
import com.matinzac.entity.PayOrder;
import com.matinzac.service.WxPayService;
import com.matinzac.util.*;
import com.matinzac.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
/**
* <p>Description:微信支付具体实现</p>
*
* @author MtinZac
* create on 2021/11/26 上午10:40
*/
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {
@Resource
private WxPayConfig wxPayConfig;
@Override
public String getMchSerialNo() {
//获取证书编号
X509Certificate certificate = getCertificate(FileUtil.getInputStream(wxPayConfig.getCertPath()));
String serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
log.info("获取的证书编号为:{}",serialNo);
return serialNo;
}
@Override
public String getOpenId(String code) {
log.info("获取code:{}", code);
String url = WxPayConstants.COUDE_OPENID_URL
+ wxPayConfig.getAppId() + "&secret=" + wxPayConfig.getSecret() + "&js_code=" + code + "&grant_type=authorization_code";
String res = HttpUtil.get(url);
System.out.println(res);
JSONObject object = JSONObject.parseObject(res);
String openid = object.getString("openid");
String unionid = object.getString("unionid");
log.info("根据code换取openId:{}", openid);
return openid;
}
@Override
public Map<String, Object> getPayInfo(String openid, String time_expire, PayOrder order) {
Map<String, Object> map = new HashMap();
// 支付的产品(小程序或者公众号,主要需要和微信支付绑定哦)
map.put("appid", wxPayConfig.getAppId());
// 支付的商户号
map.put("mchid", wxPayConfig.getMchId());
//临时写死配置
map.put("description", order.getRemarkBody());
map.put("out_trade_no", order.getOutTradeNo());
map.put("notify_url", wxPayConfig.getNotifyOrderUrl());
//判断过期时间是否为null 有过期时间则进行添加
if (!StringUtils.isEmpty(time_expire)) {
map.put("time_expire", time_expire);
}
Map<String, Object> amount = new HashMap();
//订单金额 单位分,需要* 100转换为元
amount.put("total", Integer.parseInt(order.getTotalFee()) * 100);
amount.put("currency", "CNY");
map.put("amount", amount);
// 设置小程序所需的opendi
Map<String, Object> payermap = new HashMap();
payermap.put("openid", openid);
map.put("payer", payermap);
ObjectMapper objectMapper = new ObjectMapper();
String body = null;
try {
body = objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Map<String, Object> stringObjectMap = null;
HashMap<String, Object> dataMap = null;
try {
//获取签名token
String token = getToken("POST", new URL(WxPayConstants.JSAPIURL), body);
//发送支付请求
stringObjectMap = HttpUtils.doPostWexin(WxPayConstants.JSAPIURL, body, token);
log.info("获取支付凭证信息为:" + stringObjectMap);
dataMap = getTokenJSAPI(wxPayConfig.getAppId(), String.valueOf(stringObjectMap.get("prepay_id")));
log.info("返回支付参数为:" + dataMap);
return dataMap;
} catch (Exception ex) {
log.info("返回支付信息体异常");
}
return null;
}
@Override
public Map<String, Object> callBack(Map body, HttpServletRequest request) {
log.info(DateUtils.getTime() + "微信支付回调开始");
Map<String, Object> result = new HashMap();
//1:获取微信支付回调的获取签名信息
String timestamp = request.getHeader("Wechatpay-Timestamp");
String nonce = request.getHeader("Wechatpay-Nonce");
ObjectMapper objectMapper = new ObjectMapper();
try {
// 2: 开始解析报文体
String data = objectMapper.writeValueAsString(body);
String message = timestamp + "\n" + nonce + "\n" + data + "\n";
//3:获取应答签名
String sign = request.getHeader("Wechatpay-Signature");
//4:获取平台对应的证书
String serialNo = request.getHeader("Wechatpay-Serial");
if (!wxPayConfig.certificateMap.containsKey(serialNo)) {
//获取验证签名的token 校验证书
String token = getToken("GET", new URL(WxPayConstants.CERTIFICATESURL), "");
wxPayConfig.certificateMap = refreshCertificate(token);
}
//获取证书编号验证签名
X509Certificate x509Certificate = wxPayConfig.certificateMap.get(serialNo);
if (!verify(x509Certificate, message.getBytes(), sign)) {
throw new IllegalArgumentException("微信支付签名验证失败:" + message);
}
// log.info("签名验证成功");
Map<String, String> resource = (Map) body.get("resource");
// 5:回调报文解密
AesUtil aesUtil = new AesUtil(wxPayConfig.getV3Key().getBytes());
//解密后json字符串
String decryptToString = aesUtil.decryptToString(
resource.get("associated_data").getBytes(),
resource.get("nonce").getBytes(),
resource.get("ciphertext"));
log.info("返回字符串解密前为------------->decryptToString====>{}", decryptToString);
//6:获取微信支付返回的信息
Map<String, Object> jsonData = objectMapper.readValue(decryptToString, Map.class);
log.info(DateUtils.getTime() + "微信回调携带数据解密后为:");
//7: 支付状态的判断 如果是success就代表支付成功
// 8:获取支付的交易单号,流水号,和附属参数
String out_trade_no = jsonData.get("out_trade_no").toString();
log.info("付款的订单号为:" + out_trade_no);
if ("SUCCESS".equals(jsonData.get("trade_state"))) {
//流水号
String transaction_id = jsonData.get("transaction_id").toString();
String success_time = jsonData.get("success_time").toString();
String attach = jsonData.get("attach").toString();
//根据订单号查询支付状态,如果未支付,更新支付状态 为已支付
// TODO 更新自己的订单状态为已支付,将支付信息进行存储(保存支付信息)
} else {
//支付失败或取消情况下
// TODO 支付失败或者其他状态修改订单状态或者根据微信的错误状态码进行路傲娇处理
}
result.put("code", "SUCCESS");
result.put("message", "成功");
} catch (Exception e) {
result.put("code", "fail");
result.put("message", "系统错误");
e.printStackTrace();
}
return result;
}
/**
* 参考网站 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
* 计算签名值
*
* @param appId 根据openId
* @param prepay_id 预支付交易会话标识(通过携带支付参数调用JSAPI下单接口)返回交易标识符
* @return
* @throws IOException
* @throws SignatureException
* @throws NoSuchAlgorithmException
* @throws
*/
public HashMap<String, Object> getTokenJSAPI(String appId, String prepay_id) throws IOException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
// 获取随机字符串
String nonceStr = getNonceStr();
// 获取微信小程序支付package
String packagestr = "prepay_id=" + prepay_id;
long timestamp = System.currentTimeMillis() / 1000;
//签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
String message = buildMessageTwo(appId, timestamp, nonceStr, packagestr);
//获取对应的签名
String signature = sign(message.getBytes("utf-8"));
// 组装返回
HashMap<String, Object> map = new HashMap<>();
map.put("appId", appId);
map.put("timeStamp", String.valueOf(timestamp));
map.put("nonceStr", nonceStr);
map.put("package", packagestr);
map.put("signType", "RSA");
map.put("paySign", signature);
return map;
}
/**
* 获取私钥
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
public static PrivateKey getPrivateKey(String filename) throws IOException {
System.out.println("filename:" + filename);
String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
try {
String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持RSA", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的密钥格式");
}
}
/**
* 生成token 也就是生成签名
*
* @param method 请求类型( GET, POST)
* @param url 获取token的URL 分别为下单 :https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi 查询证书 :https://api.mch.weixin.qq.com/v3/certificates
* @param body 封装的支付凭证信息包括商户ID,APPID和证书序列号
* @return
* @throws Exception
*/
public String getToken(String method, URL url, String body) throws Exception {
String nonceStr = UUID.getNonceStr();
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("utf-8"));
return "WECHATPAY2-SHA256-RSA2048 " + "mchid=\"" + wxPayConfig.getMchId() + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + wxPayConfig.getMchSerialNo() + "\","
+ "signature=\"" + signature + "\"";
}
/**
* 通过携带token获取平台证书
*
* @param token 获取证书的token https://api.mch.weixin.qq.com/v3/certificates
* @return
* @throws Exception
*/
public Map<String, X509Certificate> refreshCertificate(String token) throws Exception {
Map<String, X509Certificate> certificateMap = new HashMap();
// 1: 执行get请求
JsonNode jsonNode = HttpUtils.doGet(WxPayConstants.CERTIFICATESURL, token);
// 2: 获取平台验证的相关参数信息
JsonNode data = jsonNode.get("data");
if (data != null) {
for (int i = 0; i < data.size(); i++) {
JsonNode encrypt_certificate = data.get(i).get("encrypt_certificate");
//对关键信息进行解密
AesUtil aesUtil = new AesUtil(wxPayConfig.getV3Key().getBytes());
String associated_data = encrypt_certificate.get("associated_data").toString().replaceAll("\"", "");
String nonce = encrypt_certificate.get("nonce").toString().replaceAll("\"", "");
String ciphertext = encrypt_certificate.get("ciphertext").toString().replaceAll("\"", "");
//证书内容
String certStr = aesUtil.decryptToString(associated_data.getBytes(), nonce.getBytes(), ciphertext);
//证书内容转成证书对象
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(certStr.getBytes("utf-8"))
);
String serial_no = data.get(i).get("serial_no").toString().replaceAll("\"", "");
certificateMap.put(serial_no, x509Cert);
}
}
return certificateMap;
}
/**
* 生成签名信息
*
* @param method 请求类型( GET, POST)
* @param url 请求的URL
* @param timestamp 时间戳
* @param nonceStr 32 位的随机字符串
* @param body 封装的支付凭证信息包括商户ID,APPID和证书序列号
* @return
*/
public static String buildMessage(String method, URL url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.getPath();
if (url.getQuery() != null) {
canonicalUrl += "?" + url.getQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
/**
* 生成 32 位随机字符串
*
* @return 32 位的随机字符串
*/
public static String getNonceStr() {
return UUID.randomUUID().toString()
.replaceAll("-", "")
.substring(0, 32);
}
/**
* 验证签名
*
* @param certificate
* @param message
* @param signature
* @return
*/
public static boolean verify(X509Certificate certificate, byte[] message, String signature) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(certificate);
sign.update(message);
return sign.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名验证过程发生了错误", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的证书", e);
}
}
/**
* 拼接请求参数
*
* @param appId 小程序 id
* @param timestamp 时间戳
* @param nonceStr 通知随机字符串
* @param packag
* @return
*/
private static String buildMessageTwo(String appId, long timestamp, String nonceStr, String packag) {
return appId + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ packag + "\n";
}
/**
* 拿到私钥文件生成签名
*
* @param message
* @return
* @throws NoSuchAlgorithmException
* @throws SignatureException
* @throws IOException
* @throws InvalidKeyException
*/
private String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, IOException, InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA"); //SHA256withRSA
sign.initSign(getPrivateKey(wxPayConfig.getKeyPath()));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
}
/**
* 通过cert 文件流获取证书编号
* @param inputStream 文件输入流
* @return 编码的证书编号
*/
public static X509Certificate getCertificate(InputStream inputStream) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate)cf.generateCertificate(inputStream);
cert.checkValidity();
return cert;
} catch (CertificateExpiredException var3) {
throw new RuntimeException("证书已过期", var3);
} catch (CertificateNotYetValidException var4) {
throw new RuntimeException("证书尚未生效", var4);
} catch (CertificateException var5) {
throw new RuntimeException("无效的证书", var5);
}
}
10.编写controller暴露接口
package com.matinzac.controller;
import com.matinzac.entity.PayOrder;
import com.matinzac.res.AjaxResult;
import com.matinzac.service.WxPayService;
import com.matinzac.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* <p>Description: 微信支付相关接口</p>
*
* @author MtinZac
* create on 2021/11/26 上午10:40
*/
@Slf4j
@CrossOrigin
@RequestMapping("/app")
@RestController
public class WxPayController {
@Resource
private WxPayService payService;
/**
* 通过商户配置文件获取证书编号
*
* @return 返回证书编号
*/
@ApiOperation(value = "获取证书编号")
@GetMapping("/getMchSerialNo")
public AjaxResult getMchSerialNo() {
String mchSerialNo = payService.getMchSerialNo();
return AjaxResult.success().put("mchSerialNo", mchSerialNo);
}
/**
* 通过 code 换取 openId
*
* @param code 微信客户端code值
* @return 返回换取的openId
*/
@GetMapping("/getOpenId/{code}")
public AjaxResult getOpenId(@PathVariable String code) {
if (StringUtils.isEmpty(code)) {
AjaxResult.error("code不能为空");
}
String openId = payService.getOpenId(code);
return AjaxResult.success().put("openId", openId);
}
/**
* 通过 openId 和活动 id 获取支付信息参数
* 微信支付 jsapi 文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
* @param openid 客户端唯一标识符
* @param aid 活动ID
* @return 唤起小程序支付的相关参数(包括会话id,订单ID)
*/
@GetMapping("/getPayInfo/{openid}/{aid}")
public AjaxResult getPayInfo(@PathVariable("openid") String openid, @PathVariable("aid") String aid) {
if (StringUtils.isEmpty(openid)) {
AjaxResult.error("openid不能为空");
}
if (StringUtils.isEmpty(aid)) {
AjaxResult.error("aid 不能为空");
}
// TODO 查询订单支付状态同一笔订单或者同一个商品防止超付
PayOrder payOrder = new PayOrder();
// TODO 这里需要设置订单超时时间的可以在这里进行配置,在 dateUtils 提供了相关的方法进行时间格式转换具体参照
// 微信 JSAPI 支付订单超时时间说明 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
//获取支付参数进行返回
Map<String, Object> payInfo = payService.getPayInfo(openid, null, payOrder);
//成功取到支付返回信息后创建订单
// TODO 将订单信息进行存储
//返回支付参数唤起微信支付
return AjaxResult.success().put("payInfo", payInfo).put("order", payOrder);
}
/**
* 支付成功回调接口
*
* @param body 微信返回参数体
* @param request 请求体
* @return 给微信返回的处理信息
*/
@PostMapping("/pay/callback")
public Map orderPayCallback(@RequestBody Map body, HttpServletRequest request) {
return payService.callBack(body, request);
}
}
以上就是全部实现调用微信 JSAPI 支付的源码,有部分utils 没有放出来但是都很简单自己可实现,(DateUtils,StringUtils,UUIDUtils)所以这个几个需求强烈的请移步 gitee 码云,我放了完整源码,可直接拉下进行修改配置直接启动!
重要提示:如上实现全部调用逻辑目前本人项目在使用全部源码,Mapper 数据库操作层面没有张贴出来,故此这一部分需要自己结合业务需求进行编写就可以了,service实现里面具体的 TODO 有标明哪里需要进行数据库操作,请参照.