@ -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----- |