最近整理一下关于微信支付的内容
文档说明
代码
文件目录示意图
代码段—小程序(其他的类似)
- AesUtil --密钥加解密
/**
* @program:
* @author: zzg
* @create: 2021-07-27 10:30
*/
import cn.hutool.core.codec.Base64;
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.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* 〈一句话功能简述〉<br>
* 〈〉
*
* @author Gym
* @create 2020/12/18
* @since 1.0.0
*/
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
/**
* @param key APIv3 密钥
*/
public AesUtil(byte[] key) {
if (key.length != KEY_LENGTH_BYTE) {
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
}
this.aesKey = key;
}
/**
* 证书和回调报文解密
*
* @param associatedData associated_data
* @param nonce nonce
* @param cipherText ciphertext
* @return {String} 平台证书明文
* @throws GeneralSecurityException 异常
*/
public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException {
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.decode(cipherText)), StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
- Pay–支付工具类
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import okhttp3.HttpUrl;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
/**
* @program:
* @author: zzg
* @create: 2021-07-27 10:31
*/
public class Pay {
//调试日志
private final static Logger logger = LoggerFactory.getLogger(Pay.class);
//请求网关
private static final String url_prex = "https://api.mch.weixin.qq.com/";
//编码
private static final String charset = "UTF-8";
/**
* 微信支付下单
*
* @param url 请求地址(只需传入域名之后的路由地址)
* @param jsonStr 请求体 json字符串 此参数与微信官方文档一致
* @param mercId 商户ID
* @param serial_no 证书序列号
* @param privateKeyFilePath 私钥的路径
* @return 订单支付的参数
* @throws Exception
*/
public static JSONObject V3PayPost(String url, String jsonStr, String mercId, String serial_no, String privateKeyFilePath) throws Exception {
String body = "";
//创建httpclient对象
CloseableHttpClient client = HttpClients.createDefault();
//创建post方式请求对象
HttpPost httpPost = new HttpPost(url_prex + url);
//装填参数
StringEntity s = new StringEntity(jsonStr, charset);
s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
"application/json"));
//设置参数到请求对象中
httpPost.setEntity(s);
String post = getToken("POST", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, jsonStr);
//设置header信息
//指定报文头【Content-type】、【User-Agent】
httpPost.setHeader("Content-type", "application/json");
httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048 " + post);
//执行请求操作,并拿到结果(同步阻塞)
CloseableHttpResponse response = client.execute(httpPost);
//获取结果实体
HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定编码转换结果实体为String类型
body = EntityUtils.toString(entity, charset);
}
EntityUtils.consume(entity);
//释放链接
response.close();
logger.info("请求地址:{},请求参数:{}。返回:{}",url,jsonStr,body);
JSONObject jsonObject = JSONObject.parseObject(body);
return jsonObject;
}
/**
* 微信支付下单
*
* @param url 请求地址(只需传入域名之后的路由地址)
* @param mercId 商户ID
* @param serial_no 证书序列号
* @param privateKeyFilePath 私钥的路径
* @return 订单支付的参数
* @throws Exception
*/
public static JSONObject V3PayGet(String url,String mercId, String serial_no, String privateKeyFilePath) throws Exception {
String body = "";
//创建httpclient对象
CloseableHttpClient client = HttpClients.createDefault();
//创建post方式请求对象
HttpGet httpGet = new HttpGet(url_prex + url);
String post = getToken("GET", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, "");
//设置header信息
//指定报文头【Content-type】、【User-Agent】
httpGet.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
httpGet.setHeader("Accept", "application/json");
httpGet.setHeader("Authorization",
"WECHATPAY2-SHA256-RSA2048 " + post);
//执行请求操作,并拿到结果(同步阻塞)
CloseableHttpResponse response = client.execute(httpGet);
//获取结果实体
HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定编码转换结果实体为String类型
body = EntityUtils.toString(entity, charset);
}
EntityUtils.consume(entity);
//释放链接
response.close();
logger.info("请求地址:{}。返回:{}",url,body);
JSONObject jsonObject = JSONObject.parseObject(body);
return jsonObject;
}
/**
* 生成组装请求头
*
* @param method 请求方式
* @param url 请求地址
* @param mercId 商户ID
* @param serial_no 证书序列号
* @param privateKeyFilePath 私钥路径
* @param body 请求体
* @return 组装请求的数据
* @throws Exception
*/
static String getToken(String method, HttpUrl url, String mercId, String serial_no, String privateKeyFilePath, String body) throws Exception {
String nonceStr = UUID.randomUUID().toString().replace("-", "");
long timestamp = System.currentTimeMillis() / 1000;
String message = buildMessage(method, url, timestamp, nonceStr, body);
String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath);
return "mchid=\"" + mercId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + serial_no + "\","
+ "signature=\"" + signature + "\"";
}
/**
* 微信调起支付参数
* 返回参数如有不理解 请访问微信官方文档
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
*
* @param prepayId 微信下单返回的prepay_id
* @param appId 应用ID(appid)
* @param privateKeyFilePath 私钥的地址
* @return 当前调起支付所需的参数
* @throws Exception
*/
public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception {
String time = System.currentTimeMillis() / 1000 + "";
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String packageStr = "prepay_id=" + prepayId;
ArrayList<String> list = new ArrayList<>();
list.add(appId);
list.add(time);
list.add(nonceStr);
list.add(packageStr);
//加载签名
String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath);
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid", appId);
jsonObject.put("timeStamp", time);
jsonObject.put("nonceStr", nonceStr);
jsonObject.put("packages", packageStr);
jsonObject.put("signType", "RSA");
jsonObject.put("paySign", packageSign);
return jsonObject;
}
/**
* 处理微信异步回调
*
* @param request
* @param response
* @param privateKey 32的秘钥
*/
public static JSONObject notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception {
Map<String, String> map = new HashMap<>(12);
String result = readData(request);
// 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号
String plainText = verifyNotify(result, privateKey);
System.out.println("plainText:"+plainText);
if (StrUtil.isNotEmpty(plainText)) {
response.setStatus(200);
map.put("code", "SUCCESS");
map.put("message", "SUCCESS");
} else {
response.setStatus(500);
map.put("code", "ERROR");
map.put("message", "签名错误");
}
response.setHeader("Content-type", ContentType.JSON.toString());
response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
JSONObject jsonObject = JSONObject.parseObject(plainText);
return jsonObject;
}
/**
* 生成签名
*
* @param message 请求体
* @param privateKeyFilePath 私钥的路径
* @return 生成base64位签名信息
* @throws Exception
*/
static String sign(byte[] message, String privateKeyFilePath) throws Exception {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(getPrivateKey(privateKeyFilePath));
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
/**
* 组装签名加载
*
* @param method 请求方式
* @param url 请求地址
* @param timestamp 请求时间
* @param nonceStr 请求随机字符串
* @param body 请求体
* @return 组装的字符串
*/
static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
String canonicalUrl = url.encodedPath();
if (url.encodedQuery() != null) {
canonicalUrl += "?" + url.encodedQuery();
}
return method + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonceStr + "\n"
+ body + "\n";
}
/**
* 获取私钥。
*
* @param filename 私钥文件路径 (required)
* @return 私钥对象
*/
static PrivateKey getPrivateKey(String filename) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filename)), "UTF-8");
// String content = new String(Files.readAllBytes(new ClassPathResource(filename).getFile().toPath()), "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("无效的密钥格式");
}
}
/**
* 构造签名串
*
* @param signMessage 待签名的参数
* @return 构造后带待签名串
*/
static String buildSignMessage(ArrayList<String> signMessage) {
if (signMessage == null || signMessage.size() <= 0) {
return null;
}
StringBuilder sbf = new StringBuilder();
for (String str : signMessage) {
sbf.append(str).append("\n");
}
return sbf.toString();
}
/**
* v3 支付异步通知验证签名
*
* @param body 异步通知密文
* @param key api 密钥
* @return 异步通知明文
* @throws Exception 异常信息
*/
static String verifyNotify(String body, String key) throws Exception {
// 获取平台证书序列号
cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body);
cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource");
String cipherText = resource.getStr("ciphertext");
String nonceStr = resource.getStr("nonce");
String associatedData = resource.getStr("associated_data");
AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8));
// 密文解密
return aesUtil.decryptToString(
associatedData.getBytes(StandardCharsets.UTF_8),
nonceStr.getBytes(StandardCharsets.UTF_8),
cipherText
);
}
/**
* 处理返回对象
*
* @param request
* @return
*/
static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- MyWxPayConfig—配置
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @program:
* @author: zzg
* @create: 2021-07-26 20:25
*/
@Component
@Data
public class MyWxPayConfig {
/**
* 设置微信公众号或者小程序等的appid
*/
@Value("${wx.appid}")
private String appId;
/**
* 微信支付园区商家号
*/
@Value("${wx.mchId}")
private String mchId;
/**
* 微信支付园区商家密钥
*/
@Value("${wx.mchKey}")
private String mchKey;
/**
* apiclient_cert.p12文件的绝对路径
*/
@Value("${wx.keyPath}")
private String keyPath;
/**
* apiclient_key.pem文件的绝对路径
*/
@Value("${wx.privateKey}")
private String privateKey;
@Value("${wx.notifyUrl}")
private String notifyUrl;
//商户API证书序列号serial_no
@Value("${wx.serialNo}")
private String serialNo;
}
使用
package com.example.demo;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.pay.wx.v3.MyWxPayConfig;
import com.example.demo.pay.wx.v3.Pay;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* 微信支付test
*
* @program: demo
* @author: zzg
* @create: 2022-02-17 15:15
*/
@SpringBootTest
public class WxpayTest {
@Autowired
private MyWxPayConfig myWxPayConfig;
/**
*
* 1.获取预支付订单
* 2,生成签名支付信息
* 3.手机端调起支付
*
* @author zzg
* @date 2022/2/16
* @throws
*/
@Test
public void wxPayV3(){
String orderNo = String.valueOf(System.currentTimeMillis());
try {
//获取预支付订单
String prepayId = wxPayv3("测试", orderNo, 1, "oCGNQ5M9wji19ytIvddaRt2ing8NKws");
//生成签名支付信息
JSONObject object = Pay.WxTuneUp(prepayId,myWxPayConfig.getAppId(), myWxPayConfig.getPrivateKey());
object.put("orderNo",orderNo);
System.out.println(object);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
*
* @param description 购买描述
* @param outTradeNo 商户订单
* @param total 金额(分)
* @param openId 微信用户openId
* @author zzg
* @date 2022/2/16
* @return java.lang.String
* @throws
*/
public String wxPayv3(String description,String outTradeNo,Integer total,String openId) throws Exception {
//支付的请求参数信息(此参数与微信支付文档一致,文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml)
Map<String, Object> data = new HashMap();
data.put("description",description);
data.put("out_trade_no", outTradeNo);
data.put("notify_url", myWxPayConfig.getNotifyUrl());
HashMap<String, Object> map = new HashMap<>();
map.put("total",total);
map.put("currency","CNY");
data.put("amount", map);
HashMap<String, Object> map1 = new HashMap<>();
map1.put("openid",openId);
data.put("payer",map1);
data.put("appid",myWxPayConfig.getAppId());
data.put("mchid",myWxPayConfig.getMchId());
String wxPayRequestJsonStr = JSONUtil.toJsonStr(data);
//第一步获取prepay_id
com.alibaba.fastjson.JSONObject prepayId = Pay.V3PayPost("v3/pay/transactions/jsapi", wxPayRequestJsonStr, myWxPayConfig.getMchId(), myWxPayConfig.getSerialNo(), myWxPayConfig.getPrivateKey());
if(StringUtils.isNotBlank(prepayId.getString("prepay_id"))){
return prepayId.getString("prepay_id");
}else{
System.out.println("支付异常");
}
return "";
}
//支付回调
/**
* @PostMapping(value = "/wxnoty")
* public void wxnoty(HttpServletRequest request, HttpServletResponse response) throws Exception {
* wxnoty(request,response);
* }
*/
//回调
public void wxnoty(HttpServletRequest request, HttpServletResponse response) {
JSONObject notify=new JSONObject();
Integer dealstatus=0;
try {
notify = Pay.notify(request, response, myWxPayConfig.getMchKey());
}catch (Exception e){
}finally {
//做其他事情
}
}
//主动查询付款情况,根据商户的订单号查询
public void searchOrder() {
//自己的订单号
String orderNo="";
try {
JSONObject jsonObject = Pay.V3PayGet("v3/pay/transactions/out-trade-no/" + orderNo + "?mchid=" + myWxPayConfig.getMchId() , myWxPayConfig.getMchId(), myWxPayConfig.getSerialNo(), myWxPayConfig.getPrivateKey());
//处理其他事件,如:订单支付状态修改
} catch (Exception e) {
e.printStackTrace();
}
}
//退款
/**
* 一年内的订单退款,超过请使用 资金应用-付款 功能
* @author zzg
* @date 2022/2/17
* @throws
*/
@Test
public void refund(){
Map<String, Object> data = new HashMap();
//微信支付交易号
data.put("transaction_id","");
//自己定义的退款单号
data.put("out_refund_no", "");
data.put("reason","押金退还");
//退款回调地址
data.put("notify_url","");
HashMap<String, Object> map = new HashMap<>();
//退款金额,单位:分
map.put("refund",100);
//本次退款交易号的全部支付金额,单位:分
map.put("total",100);
map.put("currency","CNY");
data.put("amount", map);
String wxPayRequestJsonStr = JSONUtil.toJsonStr(data);
JSONObject object = null;
try {
object = Pay.V3PayPost("v3/refund/domestic/refunds", wxPayRequestJsonStr, myWxPayConfig.getMchId(), myWxPayConfig.getSerialNo(), myWxPayConfig.getPrivateKey());
System.out.println(object);
} catch (Exception e) {
e.printStackTrace();
}
}
}