WxPayConfig.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. package com.github.binarywang.wxpay.config;
  2. import com.github.binarywang.wxpay.exception.WxPayException;
  3. import com.github.binarywang.wxpay.util.HttpProxyUtils;
  4. import com.github.binarywang.wxpay.util.ResourcesUtils;
  5. import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder;
  6. import com.github.binarywang.wxpay.v3.auth.*;
  7. import com.github.binarywang.wxpay.v3.util.PemUtils;
  8. import java.io.*;
  9. import java.net.URL;
  10. import java.nio.charset.StandardCharsets;
  11. import java.security.KeyStore;
  12. import java.security.PrivateKey;
  13. import java.security.PublicKey;
  14. import java.security.cert.Certificate;
  15. import java.security.cert.X509Certificate;
  16. import java.util.Base64;
  17. import java.util.Optional;
  18. import javax.net.ssl.SSLContext;
  19. import lombok.Data;
  20. import lombok.EqualsAndHashCode;
  21. import lombok.SneakyThrows;
  22. import lombok.ToString;
  23. import lombok.extern.slf4j.Slf4j;
  24. import org.apache.commons.lang3.RegExUtils;
  25. import org.apache.commons.lang3.StringUtils;
  26. import org.apache.http.impl.client.CloseableHttpClient;
  27. import org.apache.http.ssl.SSLContexts;
  28. /**
  29. * 微信支付配置
  30. *
  31. * @author Binary Wang (<a href="https://github.com/binarywang">...</a>)
  32. */
  33. @Data
  34. @Slf4j
  35. @ToString(exclude = "verifier")
  36. @EqualsAndHashCode(exclude = "verifier")
  37. public class WxPayConfig {
  38. private static final String DEFAULT_PAY_BASE_URL = "https://api.mch.weixin.qq.com";
  39. private static final String PROBLEM_MSG = "证书文件【%s】有问题,请核实!";
  40. private static final String NOT_FOUND_MSG = "证书文件【%s】不存在,请核实!";
  41. /**
  42. * 微信支付接口请求地址域名部分.
  43. */
  44. private String payBaseUrl = DEFAULT_PAY_BASE_URL;
  45. /**
  46. * http请求连接超时时间.
  47. */
  48. private int httpConnectionTimeout = 5000;
  49. /**
  50. * http请求数据读取等待时间.
  51. */
  52. private int httpTimeout = 10000;
  53. /**
  54. * 公众号appid.
  55. */
  56. private String appId;
  57. /**
  58. * 服务商模式下的子商户公众账号ID.
  59. */
  60. private String subAppId;
  61. /**
  62. * 商户号.
  63. */
  64. private String mchId;
  65. /**
  66. * 商户密钥.
  67. */
  68. private String mchKey;
  69. /**
  70. * 企业支付密钥.
  71. */
  72. private String entPayKey;
  73. /**
  74. * 服务商模式下的子商户号.
  75. */
  76. private String subMchId;
  77. /**
  78. * 微信支付异步回掉地址,通知url必须为直接可访问的url,不能携带参数.
  79. */
  80. private String notifyUrl;
  81. /**
  82. * 交易类型.
  83. * <pre>
  84. * JSAPI--公众号支付
  85. * NATIVE--原生扫码支付
  86. * APP--app支付
  87. * </pre>
  88. */
  89. private String tradeType;
  90. /**
  91. * 签名方式.
  92. * 有两种HMAC_SHA256 和MD5
  93. *
  94. * @see com.github.binarywang.wxpay.constant.WxPayConstants.SignType
  95. */
  96. private String signType;
  97. private SSLContext sslContext;
  98. /**
  99. * p12证书base64编码
  100. */
  101. private String keyString;
  102. /**
  103. * p12证书文件的绝对路径或者以classpath:开头的类路径.
  104. */
  105. private String keyPath;
  106. /**
  107. * apiclient_key.pem证书base64编码
  108. */
  109. private String privateKeyString;
  110. /**
  111. * apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
  112. */
  113. private String privateKeyPath;
  114. /**
  115. * apiclient_cert.pem证书base64编码
  116. */
  117. private String privateCertString;
  118. /**
  119. * apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.
  120. */
  121. private String privateCertPath;
  122. /**
  123. * apiclient_key.pem证书文件内容的字节数组.
  124. */
  125. private byte[] privateKeyContent;
  126. /**
  127. * apiclient_cert.pem证书文件内容的字节数组.
  128. */
  129. private byte[] privateCertContent;
  130. /**
  131. * 公钥ID
  132. */
  133. private String publicKeyId;
  134. /**
  135. * pub_key.pem证书base64编码
  136. */
  137. private String publicKeyString;
  138. /**
  139. * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
  140. */
  141. private String publicKeyPath;
  142. /**
  143. * pub_key.pem证书文件内容的字节数组.
  144. */
  145. private byte[] publicKeyContent;
  146. /**
  147. * apiV3 秘钥值.
  148. */
  149. private String apiV3Key;
  150. /**
  151. * apiV3 证书序列号值
  152. */
  153. private String certSerialNo;
  154. /**
  155. * 微信支付分serviceId
  156. */
  157. private String serviceId;
  158. /**
  159. * 微信支付分回调地址
  160. */
  161. private String payScoreNotifyUrl;
  162. /**
  163. * 微信支付分授权回调地址
  164. */
  165. private String payScorePermissionNotifyUrl;
  166. private CloseableHttpClient apiV3HttpClient;
  167. /**
  168. * 支持扩展httpClientBuilder
  169. */
  170. private HttpClientBuilderCustomizer httpClientBuilderCustomizer;
  171. private HttpClientBuilderCustomizer apiV3HttpClientBuilderCustomizer;
  172. /**
  173. * 私钥信息
  174. */
  175. private PrivateKey privateKey;
  176. /**
  177. * 证书自动更新时间差(分钟),默认一分钟
  178. */
  179. private int certAutoUpdateTime = 60;
  180. /**
  181. * p12证书文件内容的字节数组.
  182. */
  183. private byte[] keyContent;
  184. /**
  185. * 微信支付是否使用仿真测试环境.
  186. * 默认不使用
  187. */
  188. private boolean useSandboxEnv = false;
  189. /**
  190. * 是否将接口请求日志信息保存到threadLocal中.
  191. * 默认不保存
  192. */
  193. private boolean ifSaveApiData = false;
  194. private String httpProxyHost;
  195. private Integer httpProxyPort;
  196. private String httpProxyUsername;
  197. private String httpProxyPassword;
  198. /**
  199. * v3接口下证书检验对象,通过改对象可以获取到X509Certificate,进一步对敏感信息加密
  200. * <a href="https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi">文档</a>
  201. */
  202. private Verifier verifier;
  203. /**
  204. * 返回所设置的微信支付接口请求地址域名.
  205. *
  206. * @return 微信支付接口请求地址域名
  207. */
  208. public String getPayBaseUrl() {
  209. if (StringUtils.isEmpty(this.payBaseUrl)) {
  210. return DEFAULT_PAY_BASE_URL;
  211. }
  212. return this.payBaseUrl;
  213. }
  214. @SneakyThrows
  215. public Verifier getVerifier() {
  216. if (verifier == null) {
  217. //当改对象为null时,初始化api v3的请求头
  218. initApiV3HttpClient();
  219. }
  220. return verifier;
  221. }
  222. /**
  223. * 初始化ssl.
  224. *
  225. * @return the ssl context
  226. * @throws WxPayException the wx pay exception
  227. */
  228. public SSLContext initSSLContext() throws WxPayException {
  229. if (StringUtils.isBlank(this.getMchId())) {
  230. throw new WxPayException("请确保商户号mchId已设置");
  231. }
  232. try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
  233. this.keyContent, "p12证书")) {
  234. KeyStore keystore = KeyStore.getInstance("PKCS12");
  235. char[] partnerId2charArray = this.getMchId().toCharArray();
  236. keystore.load(inputStream, partnerId2charArray);
  237. this.sslContext = SSLContexts.custom().loadKeyMaterial(keystore, partnerId2charArray).build();
  238. return this.sslContext;
  239. } catch (Exception e) {
  240. throw new WxPayException("证书文件有问题,请核实!", e);
  241. }
  242. }
  243. /**
  244. * 初始化api v3请求头 自动签名验签
  245. * 方法参照 <a href="https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient">微信支付官方api项目</a>
  246. *
  247. * @return org.apache.http.impl.client.CloseableHttpClient
  248. * @author doger.wang
  249. **/
  250. public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
  251. if (StringUtils.isBlank(this.getApiV3Key())) {
  252. throw new WxPayException("请确保apiV3Key值已设置");
  253. }
  254. // 尝试从p12证书中加载私钥和证书
  255. PrivateKey merchantPrivateKey = null;
  256. X509Certificate certificate = null;
  257. Object[] objects = this.p12ToPem();
  258. if (objects != null) {
  259. merchantPrivateKey = (PrivateKey) objects[0];
  260. certificate = (X509Certificate) objects[1];
  261. this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
  262. }
  263. try {
  264. if (merchantPrivateKey == null) {
  265. if (StringUtils.isNotBlank(this.getPrivateKeyString())) {
  266. this.setPrivateKeyString(Base64.getEncoder().encodeToString(this.getPrivateKeyString().getBytes()));
  267. }
  268. try (InputStream keyInputStream = this.loadConfigInputStream(this.getPrivateKeyString(), this.getPrivateKeyPath(),
  269. this.privateKeyContent, "privateKeyPath")) {
  270. merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream);
  271. }
  272. }
  273. if (certificate == null && StringUtils.isBlank(this.getCertSerialNo())) {
  274. try (InputStream certInputStream = this.loadConfigInputStream(this.getPrivateCertString(), this.getPrivateCertPath(),
  275. this.privateCertContent, "privateCertPath")) {
  276. certificate = PemUtils.loadCertificate(certInputStream);
  277. }
  278. this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
  279. }
  280. PublicKey publicKey = null;
  281. if (this.getPublicKeyString() != null || this.getPublicKeyPath() != null || this.publicKeyContent != null) {
  282. try (InputStream pubInputStream =
  283. this.loadConfigInputStream(this.getPublicKeyString(), this.getPublicKeyPath(),
  284. this.publicKeyContent, "publicKeyPath")) {
  285. publicKey = PemUtils.loadPublicKey(pubInputStream);
  286. }
  287. }
  288. //构造Http Proxy正向代理
  289. WxPayHttpProxy wxPayHttpProxy = getWxPayHttpProxy();
  290. Verifier certificatesVerifier;
  291. if (publicKey == null) {
  292. certificatesVerifier =
  293. new AutoUpdateCertificatesVerifier(
  294. new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
  295. this.getApiV3Key().getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime(),
  296. this.getPayBaseUrl(), wxPayHttpProxy);
  297. } else {
  298. certificatesVerifier = new PublicCertificateVerifier(publicKey, publicKeyId);
  299. }
  300. WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
  301. .withMerchant(mchId, certSerialNo, merchantPrivateKey)
  302. .withValidator(new WxPayValidator(certificatesVerifier));
  303. //初始化V3接口正向代理设置
  304. HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, wxPayHttpProxy);
  305. // 提供自定义wxPayV3HttpClientBuilder的能力
  306. Optional.ofNullable(apiV3HttpClientBuilderCustomizer).ifPresent(e -> {
  307. e.customize(wxPayV3HttpClientBuilder);
  308. });
  309. CloseableHttpClient httpClient = wxPayV3HttpClientBuilder.build();
  310. this.apiV3HttpClient = httpClient;
  311. this.verifier = certificatesVerifier;
  312. this.privateKey = merchantPrivateKey;
  313. return httpClient;
  314. } catch (WxPayException e) {
  315. throw e;
  316. } catch (Exception e) {
  317. throw new WxPayException("v3请求构造异常!", e);
  318. }
  319. }
  320. /**
  321. * 初始化一个WxPayHttpProxy对象
  322. *
  323. * @return 返回封装的WxPayHttpProxy对象。如未指定代理主机和端口,则默认返回null
  324. */
  325. private WxPayHttpProxy getWxPayHttpProxy() {
  326. if (StringUtils.isNotBlank(this.getHttpProxyHost()) && this.getHttpProxyPort() > 0) {
  327. return new WxPayHttpProxy(getHttpProxyHost(), getHttpProxyPort(), getHttpProxyUsername(), getHttpProxyPassword());
  328. }
  329. return null;
  330. }
  331. private InputStream loadConfigInputStream(String configString, String configPath, byte[] configContent,
  332. String fileName) throws WxPayException {
  333. InputStream inputStream;
  334. if (configContent != null) {
  335. inputStream = new ByteArrayInputStream(configContent);
  336. } else if (StringUtils.isNotEmpty(configString)) {
  337. configContent = configString.getBytes(StandardCharsets.UTF_8);
  338. inputStream = new ByteArrayInputStream(configContent);
  339. } else {
  340. if (StringUtils.isBlank(configPath)) {
  341. throw new WxPayException("请确保证书文件地址【" + fileName + "】或者内容已配置");
  342. }
  343. inputStream = this.loadConfigInputStream(configPath);
  344. }
  345. return inputStream;
  346. }
  347. /**
  348. * 从配置路径 加载配置 信息(支持 classpath、本地路径、网络url)
  349. *
  350. * @param configPath 配置路径
  351. * @return .
  352. * @throws WxPayException .
  353. */
  354. private InputStream loadConfigInputStream(String configPath) throws WxPayException {
  355. String fileHasProblemMsg = String.format(PROBLEM_MSG, configPath);
  356. String fileNotFoundMsg = String.format(NOT_FOUND_MSG, configPath);
  357. final String prefix = "classpath:";
  358. InputStream inputStream;
  359. if (configPath.startsWith(prefix)) {
  360. String path = RegExUtils.removeFirst(configPath, prefix);
  361. if (!path.startsWith("/")) {
  362. path = "/" + path;
  363. }
  364. try {
  365. inputStream = ResourcesUtils.getResourceAsStream(path);
  366. if (inputStream == null) {
  367. throw new WxPayException(fileNotFoundMsg);
  368. }
  369. return inputStream;
  370. } catch (Exception e) {
  371. throw new WxPayException(fileNotFoundMsg, e);
  372. }
  373. }
  374. if (configPath.startsWith("http://") || configPath.startsWith("https://")) {
  375. try {
  376. inputStream = new URL(configPath).openStream();
  377. if (inputStream == null) {
  378. throw new WxPayException(fileNotFoundMsg);
  379. }
  380. return inputStream;
  381. } catch (IOException e) {
  382. throw new WxPayException(fileNotFoundMsg, e);
  383. }
  384. } else {
  385. try {
  386. File file = new File(configPath);
  387. if (!file.exists()) {
  388. throw new WxPayException(fileNotFoundMsg);
  389. }
  390. //使用Files.newInputStream打开公私钥文件,会存在无法释放句柄的问题
  391. //return Files.newInputStream(file.toPath());
  392. return new FileInputStream(file);
  393. } catch (IOException e) {
  394. throw new WxPayException(fileHasProblemMsg, e);
  395. }
  396. }
  397. }
  398. /**
  399. * 分解p12证书文件
  400. */
  401. private Object[] p12ToPem() {
  402. String key = getMchId();
  403. if (StringUtils.isBlank(key) || StringUtils.isBlank(this.getKeyPath())) {
  404. return null;
  405. }
  406. // 分解p12证书文件
  407. try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
  408. this.keyContent, "p12证书")) {
  409. KeyStore keyStore = KeyStore.getInstance("PKCS12");
  410. keyStore.load(inputStream, key.toCharArray());
  411. String alias = keyStore.aliases().nextElement();
  412. PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, key.toCharArray());
  413. Certificate certificate = keyStore.getCertificate(alias);
  414. X509Certificate x509Certificate = (X509Certificate) certificate;
  415. return new Object[]{privateKey, x509Certificate};
  416. } catch (Exception e) {
  417. log.error("加载p12证书时发生异常", e);
  418. }
  419. return null;
  420. }
  421. }