package com.doumee.config.wx; 
 | 
  
 | 
  
 | 
import com.google.gson.ExclusionStrategy; 
 | 
import com.google.gson.FieldAttributes; 
 | 
import com.google.gson.Gson; 
 | 
import com.google.gson.GsonBuilder; 
 | 
import com.google.gson.JsonElement; 
 | 
import com.google.gson.JsonObject; 
 | 
import com.google.gson.JsonParser; 
 | 
import com.google.gson.JsonSyntaxException; 
 | 
import com.google.gson.annotations.Expose; 
 | 
import com.wechat.pay.java.core.util.GsonUtil; 
 | 
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.InvalidKeyException; 
 | 
import java.security.KeyFactory; 
 | 
import java.security.NoSuchAlgorithmException; 
 | 
import java.security.PrivateKey; 
 | 
import java.security.PublicKey; 
 | 
import java.security.SecureRandom; 
 | 
import java.security.Signature; 
 | 
import java.security.SignatureException; 
 | 
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 { 
 | 
        private static final long serialVersionUID = 2261086748874802175L; 
 | 
  
 | 
        private final int statusCode; 
 | 
        private final String body; 
 | 
        private final Headers headers; 
 | 
        private final String errorCode; 
 | 
        private final String errorMessage; 
 | 
  
 | 
        public ApiException(int statusCode, String body, Headers headers) { 
 | 
            super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers)); 
 | 
            this.statusCode = statusCode; 
 | 
            this.body = body; 
 | 
            this.headers = headers; 
 | 
  
 | 
            if (body != null && !body.isEmpty()) { 
 | 
                JsonElement code; 
 | 
                JsonElement message; 
 | 
  
 | 
                try { 
 | 
                    JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class); 
 | 
                    code = jsonObject.get("code"); 
 | 
                    message = jsonObject.get("message"); 
 | 
                } catch (JsonSyntaxException ignored) { 
 | 
                    code = null; 
 | 
                    message = null; 
 | 
                } 
 | 
                this.errorCode = code == null ? null : code.getAsString(); 
 | 
                this.errorMessage = message == null ? null : message.getAsString(); 
 | 
            } else { 
 | 
                this.errorCode = null; 
 | 
                this.errorMessage = null; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        /** 
 | 
         * 获取 HTTP 应答状态码 
 | 
         */ 
 | 
        public int getStatusCode() { 
 | 
            return statusCode; 
 | 
        } 
 | 
  
 | 
        /** 
 | 
         * 获取 HTTP 应答包体内容 
 | 
         */ 
 | 
        public String getBody() { 
 | 
            return body; 
 | 
        } 
 | 
  
 | 
        /** 
 | 
         * 获取 HTTP 应答 Header 
 | 
         */ 
 | 
        public Headers getHeaders() { 
 | 
            return headers; 
 | 
        } 
 | 
  
 | 
        /** 
 | 
         * 获取 错误码 (错误应答中的 code 字段) 
 | 
         */ 
 | 
        public String getErrorCode() { 
 | 
            return errorCode; 
 | 
        } 
 | 
  
 | 
        /** 
 | 
         * 获取 错误消息 (错误应答中的 message 字段) 
 | 
         */ 
 | 
        public String getErrorMessage() { 
 | 
            return errorMessage; 
 | 
        } 
 | 
    } 
 | 
} 
 |