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 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; @Slf4j @Service @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, PrivateKey keyPair) throws UnsupportedEncodingException, NoSuchAlgorithmException { String nonceStr = RandomStringGenerator.getRandomStringByLength(32);//随机字符串 long timestamp = System.currentTimeMillis() / 1000;//时间戳 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=\"" + AppContants.WX_CARD_NUM + "\"," + "signature=\"" + signature + "\""; } 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 fapiaoCallBack(HttpServletResponse response, HttpServletRequest request) { log.info("微信电子发票回调接口...."); Map 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; } /** * 功能描述: 验证签名 * 注意:使用微信支付平台公钥验签 * Wechatpay-Signature 微信返签名 * Wechatpay-Serial 微信平台证书序列号 * * @return java.lang.String * @author 影子 */ @SneakyThrows 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 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; } /** * 获取公私钥.通过证书 */ 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("wxP12/apiclient_cert.p12"); // File file = new File("src/main/resources/wxP12/apiclient_cert.p12"); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("PKCS12"); store.load(resource.getInputStream(), pem); // store.load(new FileInputStream(file), pem); } } } X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias); certificate.checkValidity(); // 证书的序列号 也有用 50F37206347BCC9E6AC9860DAACE52AC035F7C24 String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); // 证书的 公钥 PublicKey publicKey = certificate.getPublicKey(); // 证书的私钥 PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem); return new KeyPair(publicKey, storeKey); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " , e); } } public static void main(String[] args) { 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); } }