From ec4e65ba157bded805081aa40977b1d996fd2b21 Mon Sep 17 00:00:00 2001 From: KKSU <15274802129@163.com> Date: Wed, 17 Jan 2024 11:29:57 +0800 Subject: [PATCH] fapiao --- src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java | 331 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 files changed, 303 insertions(+), 28 deletions(-) diff --git a/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java b/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java index a35aa8d..0182525 100644 --- a/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java +++ b/src/main/java/cc/mrbird/febs/pay/service/impl/WxFaPiaoServiceImpl.java @@ -1,22 +1,50 @@ package cc.mrbird.febs.pay.service.impl; import cc.mrbird.febs.common.properties.XcxProperties; +import cc.mrbird.febs.common.utils.AppContants; import cc.mrbird.febs.common.utils.SpringContextHolder; +import cc.mrbird.febs.mall.entity.MallOrderInfo; +import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper; +import cc.mrbird.febs.pay.model.HeaderDto; import cc.mrbird.febs.pay.service.WxFaPiaoService; import cc.mrbird.febs.pay.util.RandomStringGenerator; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier; +import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; +import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; +import com.wechat.pay.contrib.apache.httpclient.notification.Notification; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler; +import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; +import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import okhttp3.HttpUrl; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPatch; +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.util.EntityUtils; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; -import org.springframework.util.Base64Utils; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.io.*; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,44 +53,290 @@ @RequiredArgsConstructor public class WxFaPiaoServiceImpl implements WxFaPiaoService { + private final MallOrderInfoMapper mallOrderInfoMapper; + private final XcxProperties xcxProperties = SpringContextHolder.getBean(XcxProperties.class); @Override - public String createAuthorization(String method, String canonicalUrl, String body, KeyPair keyPair) { + public String createAuthorization(String method, String canonicalUrl, String body, PrivateKey keyPair) throws UnsupportedEncodingException, NoSuchAlgorithmException { String nonceStr = RandomStringGenerator.getRandomStringByLength(32);//随机字符串 long timestamp = System.currentTimeMillis() / 1000;//时间戳 - String signature = sign(method, canonicalUrl, timestamp, nonceStr, body, keyPair);//签名加密 + HttpUrl httpurl = HttpUrl.parse(canonicalUrl); + String message = buildMessage(method, httpurl, timestamp, nonceStr, body); + log.info("签名串:\n"+message); + log.info("签名串长度:\n"+getWordCount(message)); + String signature = sign2(message.getBytes("utf-8"), keyPair); + + log.info("签名串sign:\n"+signature); + log.info("签名串长度sign:\n"+getWordCount(signature)); +// String yourCertificateSerialNo = "221D49AEC4EA538A63941D1936709C8559EB05C5"; return "mchid=\"" + xcxProperties.getWecharpayMchid() + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," - + "serial_no=\"" + "50F37206347BCC9E6AC9860DAACE52AC035F7C24" + "\","//证书序列号 + + "serial_no=\"" + AppContants.WX_CARD_NUM + "\"," + "signature=\"" + signature + "\""; } - @Override - public KeyPair getPrivateKey() { - return createPKCS12("Tenpay Certificate", "1658958205"); + public int getWordCount(String s) + { + int length = 0 ; + for ( int i = 0 ; i < s.length(); i ++ ) + { + int ascii = Character.codePointAt(s, i); + if (ascii >= 0 && ascii <= 255 ) + length ++ ; + else + length += 2 ; + + } + return length; + } + + public String sign2(byte[] message,PrivateKey keyPair) throws NoSuchAlgorithmException { + Signature sign = Signature.getInstance("SHA256withRSA"); + String s = null; + try { + sign.initSign(keyPair); + sign.update(message); + s = Base64.getEncoder().encodeToString(sign.sign()); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (SignatureException e) { + e.printStackTrace(); + } + return s; + } + + public 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"; + } + + @Override + public PrivateKey getPrivateKeyV3() throws IOException { + InputStream inputStream = new ClassPathResource("wxP12/apiclient_key.pem") + .getInputStream(); + + String content = new BufferedReader(new InputStreamReader(inputStream)) + .lines().collect(Collectors.joining(System.lineSeparator())); + 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("无效的密钥格式"); + } + } + + @Override + public String sendPatch(String url, String params, String token) { + String result = ""; + CloseableHttpClient httpClient = HttpClients.createDefault(); + HttpPatch httpPatch = new HttpPatch(url); + httpPatch.setHeader("Content-type", "application/json"); + httpPatch.setHeader("Charset", "utf-8"); + httpPatch.setHeader("Accept", "application/json"); + httpPatch.setHeader("Accept-Charset", "utf-8"); + httpPatch.setHeader("Authorization", token); + try { + StringEntity data = new StringEntity(params, "utf-8"); + httpPatch.setEntity(data); + HttpResponse response = httpClient.execute(httpPatch); + HttpEntity entity = response.getEntity(); + result = EntityUtils.toString(entity); + } catch (Exception e) { + result = "{\"status\":\"1\",\"error\":\"" + e.getMessage() + "\"}"; + } + return result; + } + + @Override + public String sendPost(String url, String params, String token) { + String result = ""; + int err = 0; + while (true) { + CloseableHttpClient client = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(url); + try { + httpPost.addHeader("Content-type", "application/json"); + httpPost.addHeader("Charset", "utf-8"); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Accept-Charset", "utf-8"); + httpPost.addHeader("Authorization", token); + + StringEntity data = new StringEntity(params, "utf-8"); + httpPost.setEntity(data); + HttpResponse response = client.execute(httpPost); + HttpEntity resEntity = response.getEntity(); + result = EntityUtils.toString(resEntity); + return result; + } catch (IOException e) { + result = "{\"status\":\"1\",\"errors\":\"" + e.getMessage() + "\"}"; + if (err++ > 2) { + break; + } + try { + Thread.sleep((err + 2) * 1000); + } catch (InterruptedException e1) { + result = "{\"status\":\"1\",\"errors\":\"" + e1.getMessage() + "\"}"; + } + } + } + return result; + } + + @Override + public Map<String, Object> fapiaoCallBack(HttpServletResponse response, HttpServletRequest request) { + log.info("微信电子发票回调接口...."); + Map<String,Object> map = new HashMap<>(); + try { + BufferedReader br = request.getReader(); + String str = null; + StringBuilder sb = new StringBuilder(); + while ((str = br.readLine())!=null) { + sb.append(str); + } + // 构建request,传入必要参数 + NotificationRequest requests = new NotificationRequest.Builder() + .withSerialNumber(request.getHeader("Wechatpay-Serial")) + .withNonce(request.getHeader("Wechatpay-Nonce")) + .withTimestamp(request.getHeader("Wechatpay-Timestamp")) + .withSignature(request.getHeader("Wechatpay-Signature")) + .withBody(String.valueOf(sb)) + .build(); + //验签 + NotificationHandler handler = new NotificationHandler(getVerifier(AppContants.WX_CARD_NUM), xcxProperties.getWecharpaySecretV3().getBytes(StandardCharsets.UTF_8)); + //解析请求体 + Notification notification = handler.parse(requests); + log.info("微信电子发票回调接口....解析请求体:"+notification.toString()); + String decryptData = notification.getDecryptData();//可能是支付业务的回调数据 + log.info("微信电子发票回调接口....decryptData:"+notification.toString()); + Notification.Resource resource = notification.getResource();//电子发票的回调加密数据 + log.info("微信电子发票回调接口....resource:"+notification.toString()); + + if ("FAPIAO.USER_APPLIED".equals(notification.getEventType())//用户发票抬头填写完成类型:FAPIAO.USER_APPLIED + && !"encryptresource".equals(notification.getResourceType())) {//通知的资源数据类型,确认成功通知为encryptresource。 + //解密 + AesUtil aesUtil = new AesUtil(xcxProperties.getWecharpaySecretV3().getBytes("utf-8")); + String decryptToString = aesUtil.decryptToString( + resource.getAssociatedData().getBytes("utf-8"), + resource.getNonce().getBytes("utf-8"), + resource.getCiphertext()); + log.info("微信电子发票回调接口....resource解密:"+decryptToString); + + JSONObject parseObj = JSONUtil.parseObj(decryptToString); + + log.info("微信电子发票回调接口....resource解密-JSONObject:"+parseObj); + + String mchid = String.valueOf(parseObj.get("mchid")); + String fapiao_apply_id = String.valueOf(parseObj.get("fapiao_apply_id")); + String apply_time = String.valueOf(parseObj.get("apply_time")); + MallOrderInfo mallOrderInfo = mallOrderInfoMapper.selectByOrderNo(fapiao_apply_id); + if(ObjectUtil.isNotEmpty(mallOrderInfo)){ + //省略查询订单 + //此处处理业务 + map.put("code","SUCCESS"); + map.put("message","成功"); + //消息推送成功 + return map; + } + } + map.put("code","RESOURCE_NOT_EXISTS"); + map.put("message", "订单不存在"); + return map; + }catch (Exception e) { + e.printStackTrace(); + } + map.put("code","FAIL"); + map.put("message", "失败"); + return map; + } + /** - * V3 SHA256withRSA 签名. + * 功能描述: 验证签名 + * 注意:使用微信支付平台公钥验签 + * Wechatpay-Signature 微信返签名 + * Wechatpay-Serial 微信平台证书序列号 * - * @param method 请求方法 GET POST PUT DELETE 等 - * @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1 - * @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致 - * @param nonceStr 随机字符串 要和TOKEN中的保持一致 - * @param body 请求体 GET 为 "" POST 为JSON - * @param keyPair 商户API 证书解析的密钥对 实际使用的是其中的私钥 - * @return the string + * @return java.lang.String + * @author 影子 */ @SneakyThrows - public String sign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) { - String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body) - .collect(Collectors.joining("\n", "", "\n")); - Signature sign = Signature.getInstance("SHA256withRSA"); - sign.initSign(keyPair.getPrivate()); - sign.update(signatureStr.getBytes(StandardCharsets.UTF_8)); - return Base64Utils.encodeToString(sign.sign()); + public boolean verifySign(HttpServletRequest request,String body) { + boolean verify = false; + try { + String wechatPaySignature = request.getHeader("Wechatpay-Signature"); + String wechatPayTimestamp = request.getHeader("Wechatpay-Timestamp"); + String wechatPayNonce = request.getHeader("Wechatpay-Nonce"); + String wechatPaySerial = request.getHeader("Wechatpay-Serial"); + //组装签名串 + String signStr = Stream.of(wechatPayTimestamp, wechatPayNonce, body) + .collect(Collectors.joining("\n", "", "\n")); + //获取平台证书 + AutoUpdateCertificatesVerifier verifier = getVerifier(wechatPaySerial); + //获取失败 验证失败 + if (verifier != null) { + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify(verifier.getValidCertificate()); + //放入签名串 + signature.update(signStr.getBytes(StandardCharsets.UTF_8)); + verify = signature.verify(Base64.getDecoder().decode(wechatPaySignature.getBytes())); + } + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return verify; } + + /** + * 保存微信平台证书 + */ + private static final ConcurrentHashMap<String, AutoUpdateCertificatesVerifier> verifierMap = new ConcurrentHashMap<>(); + + /** + * 功能描述:获取平台证书,自动更新 + * 注意:这个方法内置了平台证书的获取和返回值解密 + */ + public AutoUpdateCertificatesVerifier getVerifier(String mchSerialNo) { + AutoUpdateCertificatesVerifier verifier = null; + if (verifierMap.isEmpty() || !verifierMap.containsKey(mchSerialNo)) { + verifierMap.clear(); + try { + //传入证书 + PrivateKey privateKey = getPrivateKeyV3(); + //刷新 + PrivateKeySigner signer = new PrivateKeySigner(mchSerialNo, privateKey); + WechatPay2Credentials credentials = new WechatPay2Credentials(xcxProperties.getWecharpayMchid(), signer); + verifier = new AutoUpdateCertificatesVerifier(credentials + , xcxProperties.getWecharpaySecretV3().getBytes("utf-8")); + verifierMap.put(verifier.getValidCertificate().getSerialNumber()+"", verifier); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + verifier = verifierMap.get(mchSerialNo); + } + return verifier; + } + /** * 获取公私钥.通过证书 @@ -70,7 +344,8 @@ private KeyStore store; private final Object lock = new Object(); public KeyPair createPKCS12(String keyAlias, String keyPass) { - ClassPathResource resource = new ClassPathResource(xcxProperties.getCertLocalPath()); +// ClassPathResource resource = new ClassPathResource(xcxProperties.getCertLocalPath()); + ClassPathResource resource = new ClassPathResource("wxP12/apiclient_cert.p12"); // File file = new File("src/main/resources/wxP12/apiclient_cert.p12"); char[] pem = keyPass.toCharArray(); try { @@ -98,11 +373,11 @@ } public static void main(String[] args) { - try { - System.out.println(new ClassPathResource("wxP12/apiclient_cert.p12").getFile().exists()); - } catch (IOException e) { - e.printStackTrace(); - } + HeaderDto headerDto = new HeaderDto(); + headerDto.setCallback_url("https://api.blnka.cn/api/xcxPay/fapiaoCallBack"); + headerDto.setShow_fapiao_cell(true); + String parseObj = JSONUtil.parseObj(headerDto).toString(); + System.out.println(parseObj); } } -- Gitblit v1.9.1