@ -1,207 +0,0 @@ | |||||
package org.jeecg.config.pay; | |||||
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; | |||||
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; | |||||
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; | |||||
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; | |||||
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; | |||||
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; | |||||
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException; | |||||
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException; | |||||
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; | |||||
import lombok.Data; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.apache.http.impl.client.CloseableHttpClient; | |||||
import org.springframework.boot.context.properties.ConfigurationProperties; | |||||
import org.springframework.context.annotation.Bean; | |||||
import org.springframework.stereotype.Component; | |||||
import java.io.IOException; | |||||
import java.io.InputStream; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.security.GeneralSecurityException; | |||||
import java.security.PrivateKey; | |||||
/** | |||||
* @author java996.icu | |||||
* @title: WeChatPayConfig2 | |||||
* @projectName chemu | |||||
* @description: TODO | |||||
* @date 2022/12/7 16:36 | |||||
* @Version V1.0 | |||||
*/ | |||||
@Component | |||||
@Data | |||||
@Slf4j | |||||
@ConfigurationProperties(prefix = "wxpay") | |||||
public class WeChatPayConfig2 { | |||||
/** | |||||
* 应用编号 | |||||
*/ | |||||
private String appId; | |||||
/** | |||||
* 商户号 | |||||
*/ | |||||
private String mchId; | |||||
/** | |||||
* 服务商商户号 | |||||
*/ | |||||
private String slMchId; | |||||
/** | |||||
* APIv2密钥 | |||||
*/ | |||||
private String apiKey; | |||||
/** | |||||
* APIv3密钥 | |||||
*/ | |||||
private String apiV3Key; | |||||
/** | |||||
* 支付通知回调地址 | |||||
*/ | |||||
private String notifyUrl; | |||||
/** | |||||
* 退款回调地址 | |||||
*/ | |||||
private String refundNotifyUrl; | |||||
/** | |||||
* API 证书中的 key.pem | |||||
*/ | |||||
private String keyPemPath; | |||||
/** | |||||
* 商户序列号 | |||||
*/ | |||||
private String serialNo; | |||||
/** | |||||
* 微信支付V3-url前缀 | |||||
*/ | |||||
private String baseUrl; | |||||
/** | |||||
* 获取商户的私钥文件 | |||||
* @param keyPemPath | |||||
* @return | |||||
*/ | |||||
public PrivateKey getPrivateKey(String keyPemPath){ | |||||
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath); | |||||
if(inputStream==null){ | |||||
throw new RuntimeException("私钥文件不存在"); | |||||
} | |||||
return PemUtil.loadPrivateKey(inputStream); | |||||
} | |||||
// | |||||
// /** | |||||
// * 获取证书管理器实例 | |||||
// * @return | |||||
// */ | |||||
// @Bean | |||||
// public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException { | |||||
// | |||||
// log.info("获取证书管理器实例"); | |||||
// | |||||
// //获取商户私钥 | |||||
// PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
// | |||||
// //私钥签名对象 | |||||
// PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey); | |||||
// | |||||
// //身份认证对象 | |||||
// WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); | |||||
// | |||||
// // 使用定时更新的签名验证器,不需要传入证书 | |||||
// CertificatesManager certificatesManager = CertificatesManager.getInstance(); | |||||
// certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8)); | |||||
// | |||||
// return certificatesManager.getVerifier(mchId); | |||||
// } | |||||
@Bean | |||||
public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException { | |||||
log.info("开始获取微信支付证书管理器实例"); | |||||
// 验证关键参数 | |||||
log.debug("商户号: {}", mchId); | |||||
log.debug("证书序列号: {}", serialNo); | |||||
log.debug("APIv3密钥长度: {}", apiV3Key.length()); // 不记录具体密钥内容 | |||||
// 获取商户私钥 | |||||
PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
if (privateKey == null) { | |||||
throw new IllegalArgumentException("无法从路径加载私钥: " + keyPemPath); | |||||
} | |||||
// 创建签名器 | |||||
PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey); | |||||
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner); | |||||
try { | |||||
CertificatesManager certificatesManager = CertificatesManager.getInstance(); | |||||
// 显式设置域名(如果库需要) | |||||
// certificatesManager.setDomain("api.mch.weixin.qq.com"); | |||||
// 添加商户信息 | |||||
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8)); | |||||
// 获取验证器 | |||||
Verifier verifier = certificatesManager.getVerifier(mchId); | |||||
log.info("成功获取证书管理器实例"); | |||||
return verifier; | |||||
} catch (HttpCodeException e) { | |||||
log.error("微信支付API返回错误: "+ e); | |||||
throw e; | |||||
} catch (Exception e) { | |||||
log.error("获取证书管理器实例时发生错误", e); | |||||
throw e; | |||||
} | |||||
} | |||||
/** | |||||
* 获取支付http请求对象 | |||||
* @param verifier | |||||
* @return | |||||
*/ | |||||
@Bean(name = "wxPayClient") | |||||
public CloseableHttpClient getWxPayClient(Verifier verifier) { | |||||
//获取商户私钥 | |||||
PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() | |||||
.withMerchant(mchId, serialNo, privateKey) | |||||
.withValidator(new WechatPay2Validator(verifier)); | |||||
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 | |||||
return builder.build(); | |||||
} | |||||
/** | |||||
* 获取HttpClient,无需进行应答签名验证,跳过验签的流程 | |||||
*/ | |||||
@Bean(name = "wxPayNoSignClient") | |||||
public CloseableHttpClient getWxPayNoSignClient(){ | |||||
//获取商户私钥 | |||||
PrivateKey privateKey = getPrivateKey(keyPemPath); | |||||
//用于构造HttpClient | |||||
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() | |||||
//设置商户信息 | |||||
.withMerchant(mchId, serialNo, privateKey) | |||||
//无需进行签名验证、通过withValidator((response) -> true)实现 | |||||
.withValidator((response) -> true); | |||||
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 | |||||
return builder.build(); | |||||
} | |||||
} |
@ -0,0 +1,20 @@ | |||||
package org.jeecg.modules.api.service; | |||||
import io.swagger.annotations.ApiOperation; | |||||
import org.jeecg.common.api.vo.Result; | |||||
import org.springframework.web.bind.annotation.PostMapping; | |||||
import org.springframework.web.bind.annotation.RequestBody; | |||||
import org.springframework.web.bind.annotation.RequestMapping; | |||||
import org.springframework.web.bind.annotation.RequestMethod; | |||||
public interface CashoutService { | |||||
//会员中心-开通会员 | |||||
@ApiOperation(value="测试提现-提现", notes="测试提现-提现") | |||||
@RequestMapping(value = "/cashout", method = {RequestMethod.POST}) | |||||
public Result<?> cashout(); | |||||
//开通会员支付回调 | |||||
//支付回调 | |||||
@PostMapping("/cashoutNotify") | |||||
public Object cashoutNotify(@RequestBody String requestBody); | |||||
} |
@ -0,0 +1,29 @@ | |||||
package org.jeecg.modules.api.service.impl; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.jeecg.common.api.vo.Result; | |||||
import org.jeecg.modules.api.service.CashoutService; | |||||
import org.jeecg.modules.transferTest.TransferToUser; | |||||
import org.springframework.stereotype.Service; | |||||
@Slf4j | |||||
@Service | |||||
public class CashoutServiceImpl implements CashoutService { | |||||
@Override | |||||
public Result<?> cashout() { | |||||
String mchid = "1673516176"; | |||||
String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
TransferToUser transferToUser = new TransferToUser(mchid, certiticateSerialNo, privateKeyFilePath, wechatPayPublicKeyId, wechatPayPublicKeyFilePaht ); | |||||
transferToUser.run(); | |||||
return Result.OK("测试提现结束"); | |||||
} | |||||
@Override | |||||
public Object cashoutNotify(String requestBody) { | |||||
System.out.println("测试提现回调"); | |||||
return Result.OK("测试回调"); | |||||
} | |||||
} |
@ -0,0 +1,35 @@ | |||||
package org.jeecg.modules.api.yaoduapi; | |||||
import io.swagger.annotations.Api; | |||||
import io.swagger.annotations.ApiOperation; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.jeecg.common.api.vo.Result; | |||||
import org.jeecg.modules.api.service.CashoutService; | |||||
import org.springframework.web.bind.annotation.*; | |||||
import javax.annotation.Resource; | |||||
@Api(tags="测试提现接口-提现") | |||||
@RestController | |||||
@RequestMapping("/cashout") | |||||
@Slf4j | |||||
public class CashoutController { | |||||
/******************************************************************************************************************/ | |||||
//会员信息 | |||||
@Resource | |||||
private CashoutService cashoutService; | |||||
/******************************************************************************************************************/ | |||||
//会员中心-开通会员 | |||||
@ApiOperation(value="测试提现-提现", notes="测试提现-提现") | |||||
@RequestMapping(value = "/cashout", method = {RequestMethod.POST}) | |||||
public Result<?> cashout(){ | |||||
return cashoutService.cashout(); | |||||
} | |||||
//提现回调 | |||||
@PostMapping("/cashoutNotify") | |||||
public Object cashoutNotify(@RequestBody String requestBody){ | |||||
return cashoutService.cashoutNotify(requestBody); | |||||
} | |||||
} |
@ -0,0 +1,257 @@ | |||||
package org.jeecg.modules.transferTest; | |||||
import com.google.gson.annotations.SerializedName; | |||||
import okhttp3.*; | |||||
import java.io.IOException; | |||||
import java.io.UncheckedIOException; | |||||
import java.security.PrivateKey; | |||||
import java.security.PublicKey; | |||||
import java.util.ArrayList; | |||||
import java.util.List; | |||||
/** | |||||
* 发起转账 | |||||
*/ | |||||
public class TransferToUser { | |||||
private static String HOST = "https://api.mch.weixin.qq.com"; | |||||
private static String METHOD = "POST"; | |||||
private static String PATH = "/v3/fund-app/mch-transfer/transfer-bills"; | |||||
public static void main(String[] args) { | |||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756 | |||||
// String mchid = "1712378227"; | |||||
// String certiticateSerialNo ="33E9FE8076531A7C7AD401DC34E053DBD7C28E22"; | |||||
// String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key.pem"; | |||||
// String wechatPayPublicKeyId = "PUB_KEY_ID_0117123782272025033100396400002931"; | |||||
// String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key.pem"; | |||||
String mchid = "1673516176"; | |||||
String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
TransferToUser client = new TransferToUser( | |||||
mchid, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 | |||||
certiticateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 | |||||
privateKeyFilePath, // 商户API证书私钥文件路径,本地文件路径 | |||||
wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 | |||||
wechatPayPublicKeyFilePaht // 微信支付公钥文件路径,本地文件路径 | |||||
); | |||||
String appid = "wxa4d29e67e8a58d38"; | |||||
String openid = "oFzrW4migndUepy7zYgYO2YoZ5to"; | |||||
String notifyUrl = "https://admin.hhlm1688.com/api/hello/"; | |||||
TransferToUserRequest request = new TransferToUserRequest(); | |||||
request.appid = appid; | |||||
request.outBillNo = "plfk2020042013"; | |||||
request.transferSceneId = "1000"; | |||||
request.openid = openid; | |||||
request.userName = client.encrypt("唐斌"); | |||||
request.transferAmount = 400L; | |||||
request.transferRemark = "新会员开通有礼"; | |||||
request.notifyUrl = notifyUrl; | |||||
request.userRecvPerception = "现金奖励"; | |||||
request.transferSceneReportInfos = new ArrayList<>(); | |||||
{ | |||||
TransferSceneReportInfo item0 = new TransferSceneReportInfo(); | |||||
item0.infoType = "活动名称"; | |||||
item0.infoContent = "新会员有礼"; | |||||
request.transferSceneReportInfos.add(item0); | |||||
TransferSceneReportInfo item1 = new TransferSceneReportInfo(); | |||||
item1.infoType = "奖励说明"; | |||||
item1.infoContent = "注册会员抽奖一等奖"; | |||||
request.transferSceneReportInfos.add(item1); | |||||
}; | |||||
try { | |||||
TransferToUserResponse response = client.run(request); | |||||
// TODO: 请求成功,继续业务逻辑 | |||||
System.out.println(response); | |||||
} catch (WXPayUtility.ApiException e) { | |||||
// TODO: 请求失败,根据状态码执行不同的逻辑 | |||||
e.printStackTrace(); | |||||
} | |||||
} | |||||
public void run(){ | |||||
String mchid = "1673516176"; | |||||
String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
TransferToUser client = new TransferToUser( | |||||
mchid, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 | |||||
certiticateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 | |||||
privateKeyFilePath, // 商户API证书私钥文件路径,本地文件路径 | |||||
wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 | |||||
wechatPayPublicKeyFilePaht // 微信支付公钥文件路径,本地文件路径 | |||||
); | |||||
String appid = "wxa4d29e67e8a58d38"; | |||||
String openid = "oFzrW4migndUepy7zYgYO2YoZ5to"; | |||||
String notifyUrl = "https://admin.hhlm1688.com/api/cashout/cashout/"; | |||||
TransferToUserRequest request = new TransferToUserRequest(); | |||||
request.appid = appid; | |||||
request.outBillNo = "plfk2020042013"; | |||||
request.transferSceneId = "1000"; | |||||
request.openid = openid; | |||||
request.userName = client.encrypt("唐斌"); | |||||
request.transferAmount = 400L; | |||||
request.transferRemark = "新会员开通有礼"; | |||||
request.notifyUrl = notifyUrl; | |||||
request.userRecvPerception = "现金奖励"; | |||||
request.transferSceneReportInfos = new ArrayList<>(); | |||||
{ | |||||
TransferSceneReportInfo item0 = new TransferSceneReportInfo(); | |||||
item0.infoType = "活动名称"; | |||||
item0.infoContent = "新会员有礼"; | |||||
request.transferSceneReportInfos.add(item0); | |||||
TransferSceneReportInfo item1 = new TransferSceneReportInfo(); | |||||
item1.infoType = "奖励说明"; | |||||
item1.infoContent = "注册会员抽奖一等奖"; | |||||
request.transferSceneReportInfos.add(item1); | |||||
}; | |||||
try { | |||||
TransferToUserResponse response = client.run(request); | |||||
// TODO: 请求成功,继续业务逻辑 | |||||
System.out.println(response); | |||||
} catch (WXPayUtility.ApiException e) { | |||||
// TODO: 请求失败,根据状态码执行不同的逻辑 | |||||
e.printStackTrace(); | |||||
} | |||||
} | |||||
public TransferToUserResponse run(TransferToUserRequest request) { | |||||
String uri = PATH; | |||||
String reqBody = WXPayUtility.toJson(request); | |||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri); | |||||
reqBuilder.addHeader("Accept", "application/json"); | |||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId); | |||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody)); | |||||
reqBuilder.addHeader("Content-Type", "application/json"); | |||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody); | |||||
reqBuilder.method(METHOD, requestBody); | |||||
Request httpRequest = reqBuilder.build(); | |||||
// 发送HTTP请求 | |||||
OkHttpClient client = new OkHttpClient.Builder().build(); | |||||
try (Response httpResponse = client.newCall(httpRequest).execute()) { | |||||
String respBody = WXPayUtility.extractBody(httpResponse); | |||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) { | |||||
// 2XX 成功,验证应答签名 | |||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey, | |||||
httpResponse.headers(), respBody); | |||||
// 从HTTP应答报文构建返回数据 | |||||
return WXPayUtility.fromJson(respBody, TransferToUserResponse.class); | |||||
} else { | |||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers()); | |||||
} | |||||
} catch (IOException e) { | |||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e); | |||||
} | |||||
} | |||||
private final String mchid; | |||||
private final String certificateSerialNo; | |||||
private final PrivateKey privateKey; | |||||
private final String wechatPayPublicKeyId; | |||||
private final PublicKey wechatPayPublicKey; | |||||
public TransferToUser(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) { | |||||
this.mchid = mchid; | |||||
this.certificateSerialNo = certificateSerialNo; | |||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath); | |||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId; | |||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath); | |||||
} | |||||
public String encrypt(String plainText) { | |||||
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText); | |||||
} | |||||
public static class TransferToUserResponse { | |||||
@SerializedName("out_bill_no") | |||||
public String outBillNo; | |||||
@SerializedName("transfer_bill_no") | |||||
public String transferBillNo; | |||||
@SerializedName("create_time") | |||||
public String createTime; | |||||
@SerializedName("state") | |||||
public TransferBillStatus state; | |||||
@SerializedName("package_info") | |||||
public String packageInfo; | |||||
} | |||||
public enum TransferBillStatus { | |||||
@SerializedName("ACCEPTED") | |||||
ACCEPTED, | |||||
@SerializedName("PROCESSING") | |||||
PROCESSING, | |||||
@SerializedName("WAIT_USER_CONFIRM") | |||||
WAIT_USER_CONFIRM, | |||||
@SerializedName("TRANSFERING") | |||||
TRANSFERING, | |||||
@SerializedName("SUCCESS") | |||||
SUCCESS, | |||||
@SerializedName("FAIL") | |||||
FAIL, | |||||
@SerializedName("CANCELING") | |||||
CANCELING, | |||||
@SerializedName("CANCELLED") | |||||
CANCELLED | |||||
} | |||||
public static class TransferSceneReportInfo { | |||||
@SerializedName("info_type") | |||||
public String infoType; | |||||
@SerializedName("info_content") | |||||
public String infoContent; | |||||
} | |||||
public static class TransferToUserRequest { | |||||
@SerializedName("appid") | |||||
public String appid; | |||||
@SerializedName("out_bill_no") | |||||
public String outBillNo; | |||||
@SerializedName("transfer_scene_id") | |||||
public String transferSceneId; | |||||
@SerializedName("openid") | |||||
public String openid; | |||||
@SerializedName("user_name") | |||||
public String userName; | |||||
@SerializedName("transfer_amount") | |||||
public Long transferAmount; | |||||
@SerializedName("transfer_remark") | |||||
public String transferRemark; | |||||
@SerializedName("notify_url") | |||||
public String notifyUrl; | |||||
@SerializedName("user_recv_perception") | |||||
public String userRecvPerception; | |||||
@SerializedName("transfer_scene_report_infos") | |||||
public List<TransferSceneReportInfo> transferSceneReportInfos; | |||||
} | |||||
} |
@ -0,0 +1,381 @@ | |||||
package org.jeecg.modules.transferTest; | |||||
import com.google.gson.*; | |||||
import com.google.gson.annotations.Expose; | |||||
import okhttp3.Headers; | |||||
import okhttp3.Response; | |||||
import okio.BufferedSource; | |||||
import javax.crypto.BadPaddingException; | |||||
import javax.crypto.Cipher; | |||||
import javax.crypto.IllegalBlockSizeException; | |||||
import javax.crypto.NoSuchPaddingException; | |||||
import java.io.IOException; | |||||
import java.io.UncheckedIOException; | |||||
import java.io.UnsupportedEncodingException; | |||||
import java.net.URLEncoder; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.nio.file.Files; | |||||
import java.nio.file.Paths; | |||||
import java.security.*; | |||||
import java.security.spec.InvalidKeySpecException; | |||||
import java.security.spec.PKCS8EncodedKeySpec; | |||||
import java.security.spec.X509EncodedKeySpec; | |||||
import java.time.DateTimeException; | |||||
import java.time.Duration; | |||||
import java.time.Instant; | |||||
import java.util.Base64; | |||||
import java.util.Map; | |||||
import java.util.Objects; | |||||
public class WXPayUtility { | |||||
private static final Gson gson = new GsonBuilder() | |||||
.disableHtmlEscaping() | |||||
.addSerializationExclusionStrategy(new ExclusionStrategy() { | |||||
@Override | |||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) { | |||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class); | |||||
return expose != null && !expose.serialize(); | |||||
} | |||||
@Override | |||||
public boolean shouldSkipClass(Class<?> aClass) { | |||||
return false; | |||||
} | |||||
}) | |||||
.addDeserializationExclusionStrategy(new ExclusionStrategy() { | |||||
@Override | |||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) { | |||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class); | |||||
return expose != null && !expose.deserialize(); | |||||
} | |||||
@Override | |||||
public boolean shouldSkipClass(Class<?> aClass) { | |||||
return false; | |||||
} | |||||
}) | |||||
.create(); | |||||
private static final char[] SYMBOLS = | |||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); | |||||
private static final SecureRandom random = new SecureRandom(); | |||||
/** | |||||
* 将 Object 转换为 JSON 字符串 | |||||
*/ | |||||
public static String toJson(Object object) { | |||||
return gson.toJson(object); | |||||
} | |||||
/** | |||||
* 将 JSON 字符串解析为特定类型的实例 | |||||
*/ | |||||
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { | |||||
return gson.fromJson(json, classOfT); | |||||
} | |||||
/** | |||||
* 从公私钥文件路径中读取文件内容 | |||||
* | |||||
* @param keyPath 文件路径 | |||||
* @return 文件内容 | |||||
*/ | |||||
private static String readKeyStringFromPath(String keyPath) { | |||||
try { | |||||
return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); | |||||
} catch (IOException e) { | |||||
throw new UncheckedIOException(e); | |||||
} | |||||
} | |||||
/** | |||||
* 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 | |||||
* | |||||
* @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 | |||||
* @return PrivateKey 对象 | |||||
*/ | |||||
public static PrivateKey loadPrivateKeyFromString(String keyString) { | |||||
try { | |||||
keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") | |||||
.replace("-----END PRIVATE KEY-----", "") | |||||
.replaceAll("\\s+", ""); | |||||
return KeyFactory.getInstance("RSA").generatePrivate( | |||||
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
throw new UnsupportedOperationException(e); | |||||
} catch (InvalidKeySpecException e) { | |||||
throw new IllegalArgumentException(e); | |||||
} | |||||
} | |||||
/** | |||||
* 从 PKCS#8 格式的私钥文件中加载私钥 | |||||
* | |||||
* @param keyPath 私钥文件路径 | |||||
* @return PrivateKey 对象 | |||||
*/ | |||||
public static PrivateKey loadPrivateKeyFromPath(String keyPath) { | |||||
return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); | |||||
} | |||||
/** | |||||
* 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 | |||||
* | |||||
* @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 | |||||
* @return PublicKey 对象 | |||||
*/ | |||||
public static PublicKey loadPublicKeyFromString(String keyString) { | |||||
try { | |||||
keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") | |||||
.replace("-----END PUBLIC KEY-----", "") | |||||
.replaceAll("\\s+", ""); | |||||
return KeyFactory.getInstance("RSA").generatePublic( | |||||
new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
throw new UnsupportedOperationException(e); | |||||
} catch (InvalidKeySpecException e) { | |||||
throw new IllegalArgumentException(e); | |||||
} | |||||
} | |||||
/** | |||||
* 从 PKCS#8 格式的公钥文件中加载公钥 | |||||
* | |||||
* @param keyPath 公钥文件路径 | |||||
* @return PublicKey 对象 | |||||
*/ | |||||
public static PublicKey loadPublicKeyFromPath(String keyPath) { | |||||
return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); | |||||
} | |||||
/** | |||||
* 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 | |||||
*/ | |||||
public static String createNonce(int length) { | |||||
char[] buf = new char[length]; | |||||
for (int i = 0; i < length; ++i) { | |||||
buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; | |||||
} | |||||
return new String(buf); | |||||
} | |||||
/** | |||||
* 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 | |||||
* | |||||
* @param publicKey 加密用公钥对象 | |||||
* @param plaintext 待加密明文 | |||||
* @return 加密后密文 | |||||
*/ | |||||
public static String encrypt(PublicKey publicKey, String plaintext) { | |||||
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; | |||||
try { | |||||
Cipher cipher = Cipher.getInstance(transformation); | |||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey); | |||||
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); | |||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | |||||
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); | |||||
} catch (InvalidKeyException e) { | |||||
throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); | |||||
} catch (BadPaddingException | IllegalBlockSizeException e) { | |||||
throw new IllegalArgumentException("Plaintext is too long", e); | |||||
} | |||||
} | |||||
/** | |||||
* 使用私钥按照指定算法进行签名 | |||||
* | |||||
* @param message 待签名串 | |||||
* @param algorithm 签名算法,如 SHA256withRSA | |||||
* @param privateKey 签名用私钥对象 | |||||
* @return 签名结果 | |||||
*/ | |||||
public static String sign(String message, String algorithm, PrivateKey privateKey) { | |||||
byte[] sign; | |||||
try { | |||||
Signature signature = Signature.getInstance(algorithm); | |||||
signature.initSign(privateKey); | |||||
signature.update(message.getBytes(StandardCharsets.UTF_8)); | |||||
sign = signature.sign(); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); | |||||
} catch (InvalidKeyException e) { | |||||
throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); | |||||
} catch (SignatureException e) { | |||||
throw new RuntimeException("An error occurred during the sign process.", e); | |||||
} | |||||
return Base64.getEncoder().encodeToString(sign); | |||||
} | |||||
/** | |||||
* 使用公钥按照特定算法验证签名 | |||||
* | |||||
* @param message 待签名串 | |||||
* @param signature 待验证的签名内容 | |||||
* @param algorithm 签名算法,如:SHA256withRSA | |||||
* @param publicKey 验签用公钥对象 | |||||
* @return 签名验证是否通过 | |||||
*/ | |||||
public static boolean verify(String message, String signature, String algorithm, | |||||
PublicKey publicKey) { | |||||
try { | |||||
Signature sign = Signature.getInstance(algorithm); | |||||
sign.initVerify(publicKey); | |||||
sign.update(message.getBytes(StandardCharsets.UTF_8)); | |||||
return sign.verify(Base64.getDecoder().decode(signature)); | |||||
} catch (SignatureException e) { | |||||
return false; | |||||
} catch (InvalidKeyException e) { | |||||
throw new IllegalArgumentException("verify uses an illegal publickey.", e); | |||||
} catch (NoSuchAlgorithmException e) { | |||||
throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); | |||||
} | |||||
} | |||||
/** | |||||
* 根据微信支付APIv3请求签名规则构造 Authorization 签名 | |||||
* | |||||
* @param mchid 商户号 | |||||
* @param certificateSerialNo 商户API证书序列号 | |||||
* @param privateKey 商户API证书私钥 | |||||
* @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE | |||||
* @param uri 请求接口的URL | |||||
* @param body 请求接口的Body | |||||
* @return 构造好的微信支付APIv3 Authorization 头 | |||||
*/ | |||||
public static String buildAuthorization(String mchid, String certificateSerialNo, | |||||
PrivateKey privateKey, | |||||
String method, String uri, String body) { | |||||
String nonce = createNonce(32); | |||||
long timestamp = Instant.now().getEpochSecond(); | |||||
String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, | |||||
body == null ? "" : body); | |||||
String signature = sign(message, "SHA256withRSA", privateKey); | |||||
return String.format( | |||||
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + | |||||
"timestamp=\"%d\",serial_no=\"%s\"", | |||||
mchid, nonce, signature, timestamp, certificateSerialNo); | |||||
} | |||||
/** | |||||
* 对参数进行 URL 编码 | |||||
* | |||||
* @param content 参数内容 | |||||
* @return 编码后的内容 | |||||
*/ | |||||
public static String urlEncode(String content) { | |||||
try { | |||||
return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); | |||||
} catch (UnsupportedEncodingException e) { | |||||
throw new RuntimeException(e); | |||||
} | |||||
} | |||||
/** | |||||
* 对参数Map进行 URL 编码,生成 QueryString | |||||
* | |||||
* @param params Query参数Map | |||||
* @return QueryString | |||||
*/ | |||||
public static String urlEncode(Map<String, Object> params) { | |||||
if (params == null || params.isEmpty()) { | |||||
return ""; | |||||
} | |||||
int index = 0; | |||||
StringBuilder result = new StringBuilder(); | |||||
for (Map.Entry<String, Object> entry : params.entrySet()) { | |||||
result.append(entry.getKey()) | |||||
.append("=") | |||||
.append(urlEncode(entry.getValue().toString())); | |||||
index++; | |||||
if (index < params.size()) { | |||||
result.append("&"); | |||||
} | |||||
} | |||||
return result.toString(); | |||||
} | |||||
/** | |||||
* 从应答中提取 Body | |||||
* | |||||
* @param response HTTP 请求应答对象 | |||||
* @return 应答中的Body内容,Body为空时返回空字符串 | |||||
*/ | |||||
public static String extractBody(Response response) { | |||||
if (response.body() == null) { | |||||
return ""; | |||||
} | |||||
try { | |||||
BufferedSource source = response.body().source(); | |||||
return source.readUtf8(); | |||||
} catch (IOException e) { | |||||
throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); | |||||
} | |||||
} | |||||
/** | |||||
* 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 | |||||
* | |||||
* @param wechatpayPublicKeyId 微信支付公钥ID | |||||
* @param wechatpayPublicKey 微信支付公钥对象 | |||||
* @param headers 微信支付应答 Header 列表 | |||||
* @param body 微信支付应答 Body | |||||
*/ | |||||
public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, | |||||
Headers headers, | |||||
String body) { | |||||
String timestamp = headers.get("Wechatpay-Timestamp"); | |||||
try { | |||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); | |||||
// 拒绝过期请求 | |||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { | |||||
throw new IllegalArgumentException( | |||||
String.format("Validate http response,timestamp[%s] of httpResponse is expires, " | |||||
+ "request-id[%s]", | |||||
timestamp, headers.get("Request-ID"))); | |||||
} | |||||
} catch (DateTimeException | NumberFormatException e) { | |||||
throw new IllegalArgumentException( | |||||
String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + | |||||
"request-id[%s]", timestamp, | |||||
headers.get("Request-ID"))); | |||||
} | |||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), | |||||
body == null ? "" : body); | |||||
String serialNumber = headers.get("Wechatpay-Serial"); | |||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { | |||||
throw new IllegalArgumentException( | |||||
String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, | |||||
serialNumber)); | |||||
} | |||||
String signature = headers.get("Wechatpay-Signature"); | |||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); | |||||
if (!success) { | |||||
throw new IllegalArgumentException( | |||||
String.format("Validate response failed,the WechatPay signature is incorrect.%n" | |||||
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", | |||||
headers.get("Request-ID"), headers, body)); | |||||
} | |||||
} | |||||
/** | |||||
* 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 | |||||
*/ | |||||
public static class ApiException extends RuntimeException { | |||||
public final int statusCode; | |||||
public final String body; | |||||
public final Headers headers; | |||||
public ApiException(int statusCode, String body, Headers headers) { | |||||
super(String.format("微信支付API访问失败,StatusCode: %s, Body: %s", statusCode, body)); | |||||
this.statusCode = statusCode; | |||||
this.body = body; | |||||
this.headers = headers; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,13 @@ | |||||
package org.jeecg.modules.transferTest; | |||||
public class test { | |||||
public static void main(String[] args) { | |||||
String mchid = "1673516176"; | |||||
String certiticateSerialNo ="525CDBD76E640EFB008288572C97D2715F3F18B2"; | |||||
String privateKeyFilePath = "jeecg-boot-module-system/src/main/resources/apiclient_key_yaodu.pem"; | |||||
String wechatPayPublicKeyId = "PUB_KEY_ID_0116735161762025040100448900000949"; | |||||
String wechatPayPublicKeyFilePaht = "jeecg-boot-module-system/src/main/resources/pub_key_yaodu.pem"; | |||||
TransferToUser transferToUser = new TransferToUser(mchid, certiticateSerialNo, privateKeyFilePath, wechatPayPublicKeyId, wechatPayPublicKeyFilePaht ); | |||||
transferToUser.run(); | |||||
} | |||||
} |
@ -0,0 +1,28 @@ | |||||
-----BEGIN PRIVATE KEY----- | |||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDFHyPaZuXY6GCd | |||||
mC/CId2jMQ3Iun2cZycw2LxdWLNwtKGYKVk+GWa4upm60Zl+qrNpLam3qTD5Oyy3 | |||||
T8Mz9EWHwBYr2xTSKINRbIjabdVyV0/H2RLitwRg3XyHQPxYFcvI9t9F2GHELsAA | |||||
qGmFp0+PiCVMzT+nB3sU+yLaNyiMC8rcprjh4tBV857K3Y5rzljbJsxa9WVzS0EM | |||||
GQRwPV2LaWPApcGZIKJLRAUa8yGMKMINyb9iJjdA1/4vD8tLWmvFhXtTYmrWmFhu | |||||
5QsyKDClOek2gqGgxDL9Hvvx5PYR59Mg57kyz0vSKhYa0MKgsFLuu7QJOFXk2bJo | |||||
DJymcLypAgMBAAECggEBAL+1626riHsOdXiP3FLYEPB38sn35dZI1GrDP18ht1Kz | |||||
uj18aVjl52tdv8lbtAbnCZoPWPJQUFr0XCbkIhrTRRQjkuyQI43I7P4xql+VVnPf | |||||
yq24xo9MI6v5fPUmFMWuXQVUZA1PxrXAKef54raj49LaPDyXmYJe2iurm1fTMVIR | |||||
Jba1rBi0hp5W1fMCiCPk1SMfANTE0UOTYdSRhNVSgf51aJHFI+FR8iSmf8rEv+B0 | |||||
s8uDXvxTGgbEaZsO7wcjQfsbQixoqEmfTzEoBTHkQ1npigG32gsz8QO6P4dhJRX7 | |||||
WJ3N8x0vgJqxCLeRLEb6c8ZJ62SuEi6PtETVWXujeCECgYEA/gP8fyp5lBKF/weC | |||||
UzMCEAJzwTKjmC0IOWnoIwQTSKZTCKHmz5LMNYn3nTf2d8LpbB9GYmBhUdiO/88f | |||||
AZ/vbuskkoH0rmOgelVx13tDaVuNOOVmtKmKvzYOxiGeL6ooX2cwfRnVOMIPTBFM | |||||
G1lAzxeowTy5uLKvuuMGBVJj7MUCgYEAxqleqjcp3+4QoE9Z/2hEt6sYnI/I1T/h | |||||
kbgTO8wQSGG8ambl/qJu+9ON/Ag1jK2amrNKP1vXbTQsjkUby6U5MgVwGDB4bNN1 | |||||
+wgwxMXJ74QJtbnXp7XBqC96S6Pyw+cLEjvWk0xJ14Os2oCO1OvZibu5KHrsPECn | |||||
3OmddJkjFpUCgYEAoiwHW2TRxCBjXiP8J4QMUA5QusrKuVAezRD5fMmQSjSuFHfQ | |||||
9Tsilxfjd4OQHnvZLQd2lz4zQ96/xUAF6rKiWa1UZxkDDwdaIGBG0yzGKBCkQ+vp | |||||
u3P2ugcYPZSe+o1nQymNQoFoqNj0jTsJ3PgJsW3Idr5/UBT8rpNcd69XToUCgYB8 | |||||
AQTCIyTUTnm6V03KC3+5VedK8sVdtz5KAyieTsZrJ/bAQ/KUezfjoS4jf8xNP6Ad | |||||
qIRUADP8SnD1bVXoS/3jp1lNABRreaNPStGGQh/GjhixgouGeAGlxd0EkhXbCsDy | |||||
ZL+PujLtf5fJ3C1L4twrCS6OggwroAAn+Pr76Qrp8QKBgQD9uH47NRToKkaTPflY | |||||
mVlbLKCWnAjoqlxHrANjM6lOeC4SfAYaZRIpCK9B7LFR7eeqIb0Y/RjfwtLdFKQF | |||||
1hVCnLYTsEJ5gG64qqInVb4e0FCF/2w1uoBGwB2pUWjd0DUXH3yHc3LVCBhxOkRa | |||||
oULtLnG34V6fFclMh3Stk/pMVg== | |||||
-----END PRIVATE KEY----- |
@ -0,0 +1,9 @@ | |||||
-----BEGIN PUBLIC KEY----- | |||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxK2wqQooeGKkAQchZdwK | |||||
U3qFiPSyf83l7USGGAdEhb78iq0Dvi7RfgdHuqSVBv0fnSQJicNO+s10ofi9waxl | |||||
SCPnuO8t9MeOuS4IpZ54VWY/9pJ5A2Z0x49L0djoFWStFCpKzsg2fWBvc/7kYVFr | |||||
nq/jFyRho8/GZtxL9RLZjWLyfnpe+erxSNFEnQLoW6LC4D5L0w9oiHboHmN9Igzc | |||||
uB6pIuMwccImX1xPeu/jx2QMYrxAW/2bW3e6z4ojQWqhRtFq55INnXLV8VXE7rwI | |||||
1L0RB4R39JOsKroVE/g7SiHRbGEem2BbNaAFhjCMuZzpWSCFQkTlxLJMxxTt9j0S | |||||
+wIDAQAB | |||||
-----END PUBLIC KEY----- |