Java
更新时间:2025.05.28一、概述
本工具类 WXPayUtility 为使用 Java 接入微信支付的开发者提供了一系列实用的功能,包括 JSON 处理、密钥加载、加密签名、请求头构建、响应验证等。通过使用这个工具类,开发者可以更方便地完成与微信支付相关的开发工作。
二、安装(引入依赖的第三方库)
本工具类依赖以下第三方库:
Google Gson:用于 JSON 数据的序列化和反序列化。
OkHttp:用于 HTTP 请求处理。
你可以通过 Maven 或 Gradle 来引入这些依赖。
如果你使用的 Gradle,请在 build.gradle 中加入:
1implementation 'com.google.code.gson:gson:${VERSION}' 2implementation 'com.squareup.okhttp3:okhttp:${VERSION}'
如果你使用的 Maven,请在 pom.xml 中加入:
1<!-- Google Gson --> 2<dependency> 3 <groupId>com.google.code.gson</groupId> 4 <artifactId>gson</artifactId> 5 <version>${VERSION}</version> 6</dependency> 7<!-- OkHttp --> 8<dependency> 9 <groupId>com.squareup.okhttp3</groupId> 10 <artifactId>okhttp</artifactId> 11 <version>${VERSION}</version> 12</dependency>
三、必需的证书和密钥
运行 SDK 必需以下的商户身份信息,用于构造请求的签名和验证应答的签名:
四、工具类代码
1package com.java.utils; 2 3import com.google.gson.ExclusionStrategy; 4import com.google.gson.FieldAttributes; 5import com.google.gson.Gson; 6import com.google.gson.GsonBuilder; 7import com.google.gson.JsonSyntaxException; 8import com.google.gson.annotations.Expose; 9import okhttp3.Headers; 10import okhttp3.Response; 11import okio.BufferedSource; 12 13import javax.crypto.BadPaddingException; 14import javax.crypto.Cipher; 15import javax.crypto.IllegalBlockSizeException; 16import javax.crypto.NoSuchPaddingException; 17import java.io.IOException; 18import java.io.UncheckedIOException; 19import java.io.UnsupportedEncodingException; 20import java.net.URLEncoder; 21import java.nio.charset.StandardCharsets; 22import java.nio.file.Files; 23import java.nio.file.Paths; 24import java.security.InvalidKeyException; 25import java.security.KeyFactory; 26import java.security.NoSuchAlgorithmException; 27import java.security.PrivateKey; 28import java.security.PublicKey; 29import java.security.SecureRandom; 30import java.security.Signature; 31import java.security.SignatureException; 32import java.security.spec.InvalidKeySpecException; 33import java.security.spec.PKCS8EncodedKeySpec; 34import java.security.spec.X509EncodedKeySpec; 35import java.time.DateTimeException; 36import java.time.Duration; 37import java.time.Instant; 38import java.util.Base64; 39import java.util.Map; 40import java.util.Objects; 41 42public class WXPayUtility { 43 private static final Gson gson = new GsonBuilder() 44 .disableHtmlEscaping() 45 .addSerializationExclusionStrategy(new ExclusionStrategy() { 46 @Override 47 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 48 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 49 return expose != null && !expose.serialize(); 50 } 51 52 @Override 53 public boolean shouldSkipClass(Class<?> aClass) { 54 return false; 55 } 56 }) 57 .addDeserializationExclusionStrategy(new ExclusionStrategy() { 58 @Override 59 public boolean shouldSkipField(FieldAttributes fieldAttributes) { 60 final Expose expose = fieldAttributes.getAnnotation(Expose.class); 61 return expose != null && !expose.deserialize(); 62 } 63 64 @Override 65 public boolean shouldSkipClass(Class<?> aClass) { 66 return false; 67 } 68 }) 69 .create(); 70 private static final char[] SYMBOLS = 71 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); 72 private static final SecureRandom random = new SecureRandom(); 73 74 /** 75 * 将 Object 转换为 JSON 字符串 76 */ 77 public static String toJson(Object object) { 78 return gson.toJson(object); 79 } 80 81 /** 82 * 将 JSON 字符串解析为特定类型的实例 83 */ 84 public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 85 return gson.fromJson(json, classOfT); 86 } 87 88 /** 89 * 从公私钥文件路径中读取文件内容 90 * 91 * @param keyPath 文件路径 92 * @return 文件内容 93 */ 94 private static String readKeyStringFromPath(String keyPath) { 95 try { 96 return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); 97 } catch (IOException e) { 98 throw new UncheckedIOException(e); 99 } 100 } 101 102 /** 103 * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 104 * 105 * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 106 * @return PrivateKey 对象 107 */ 108 public static PrivateKey loadPrivateKeyFromString(String keyString) { 109 try { 110 keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") 111 .replace("-----END PRIVATE KEY-----", "") 112 .replaceAll("\\s+", ""); 113 return KeyFactory.getInstance("RSA").generatePrivate( 114 new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); 115 } catch (NoSuchAlgorithmException e) { 116 throw new UnsupportedOperationException(e); 117 } catch (InvalidKeySpecException e) { 118 throw new IllegalArgumentException(e); 119 } 120 } 121 122 /** 123 * 从 PKCS#8 格式的私钥文件中加载私钥 124 * 125 * @param keyPath 私钥文件路径 126 * @return PrivateKey 对象 127 */ 128 public static PrivateKey loadPrivateKeyFromPath(String keyPath) { 129 return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); 130 } 131 132 /** 133 * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 134 * 135 * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 136 * @return PublicKey 对象 137 */ 138 public static PublicKey loadPublicKeyFromString(String keyString) { 139 try { 140 keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") 141 .replace("-----END PUBLIC KEY-----", "") 142 .replaceAll("\\s+", ""); 143 return KeyFactory.getInstance("RSA").generatePublic( 144 new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); 145 } catch (NoSuchAlgorithmException e) { 146 throw new UnsupportedOperationException(e); 147 } catch (InvalidKeySpecException e) { 148 throw new IllegalArgumentException(e); 149 } 150 } 151 152 /** 153 * 从 PKCS#8 格式的公钥文件中加载公钥 154 * 155 * @param keyPath 公钥文件路径 156 * @return PublicKey 对象 157 */ 158 public static PublicKey loadPublicKeyFromPath(String keyPath) { 159 return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); 160 } 161 162 /** 163 * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 164 */ 165 public static String createNonce(int length) { 166 char[] buf = new char[length]; 167 for (int i = 0; i < length; ++i) { 168 buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; 169 } 170 return new String(buf); 171 } 172 173 /** 174 * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 175 * 176 * @param publicKey 加密用公钥对象 177 * @param plaintext 待加密明文 178 * @return 加密后密文 179 */ 180 public static String encrypt(PublicKey publicKey, String plaintext) { 181 final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 182 183 try { 184 Cipher cipher = Cipher.getInstance(transformation); 185 cipher.init(Cipher.ENCRYPT_MODE, publicKey); 186 return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); 187 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 188 throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); 189 } catch (InvalidKeyException e) { 190 throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); 191 } catch (BadPaddingException | IllegalBlockSizeException e) { 192 throw new IllegalArgumentException("Plaintext is too long", e); 193 } 194 } 195 196 /** 197 * 使用私钥按照指定算法进行签名 198 * 199 * @param message 待签名串 200 * @param algorithm 签名算法,如 SHA256withRSA 201 * @param privateKey 签名用私钥对象 202 * @return 签名结果 203 */ 204 public static String sign(String message, String algorithm, PrivateKey privateKey) { 205 byte[] sign; 206 try { 207 Signature signature = Signature.getInstance(algorithm); 208 signature.initSign(privateKey); 209 signature.update(message.getBytes(StandardCharsets.UTF_8)); 210 sign = signature.sign(); 211 } catch (NoSuchAlgorithmException e) { 212 throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); 213 } catch (InvalidKeyException e) { 214 throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); 215 } catch (SignatureException e) { 216 throw new RuntimeException("An error occurred during the sign process.", e); 217 } 218 return Base64.getEncoder().encodeToString(sign); 219 } 220 221 /** 222 * 使用公钥按照特定算法验证签名 223 * 224 * @param message 待签名串 225 * @param signature 待验证的签名内容 226 * @param algorithm 签名算法,如:SHA256withRSA 227 * @param publicKey 验签用公钥对象 228 * @return 签名验证是否通过 229 */ 230 public static boolean verify(String message, String signature, String algorithm, 231 PublicKey publicKey) { 232 try { 233 Signature sign = Signature.getInstance(algorithm); 234 sign.initVerify(publicKey); 235 sign.update(message.getBytes(StandardCharsets.UTF_8)); 236 return sign.verify(Base64.getDecoder().decode(signature)); 237 } catch (SignatureException e) { 238 return false; 239 } catch (InvalidKeyException e) { 240 throw new IllegalArgumentException("verify uses an illegal publickey.", e); 241 } catch (NoSuchAlgorithmException e) { 242 throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); 243 } 244 } 245 246 /** 247 * 根据微信支付APIv3请求签名规则构造 Authorization 签名 248 * 249 * @param mchid 商户号 250 * @param certificateSerialNo 商户API证书序列号 251 * @param privateKey 商户API证书私钥 252 * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE 253 * @param uri 请求接口的URL 254 * @param body 请求接口的Body 255 * @return 构造好的微信支付APIv3 Authorization 头 256 */ 257 public static String buildAuthorization(String mchid, String certificateSerialNo, 258 PrivateKey privateKey, 259 String method, String uri, String body) { 260 String nonce = createNonce(32); 261 long timestamp = Instant.now().getEpochSecond(); 262 263 String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, 264 body == null ? "" : body); 265 266 String signature = sign(message, "SHA256withRSA", privateKey); 267 268 return String.format( 269 "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + 270 "timestamp=\"%d\",serial_no=\"%s\"", 271 mchid, nonce, signature, timestamp, certificateSerialNo); 272 } 273 274 /** 275 * 对参数进行 URL 编码 276 * 277 * @param content 参数内容 278 * @return 编码后的内容 279 */ 280 public static String urlEncode(String content) { 281 try { 282 return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); 283 } catch (UnsupportedEncodingException e) { 284 throw new RuntimeException(e); 285 } 286 } 287 288 /** 289 * 对参数Map进行 URL 编码,生成 QueryString 290 * 291 * @param params Query参数Map 292 * @return QueryString 293 */ 294 public static String urlEncode(Map<String, Object> params) { 295 if (params == null || params.isEmpty()) { 296 return ""; 297 } 298 299 int index = 0; 300 StringBuilder result = new StringBuilder(); 301 for (Map.Entry<String, Object> entry : params.entrySet()) { 302 result.append(entry.getKey()) 303 .append("=") 304 .append(urlEncode(entry.getValue().toString())); 305 index++; 306 if (index < params.size()) { 307 result.append("&"); 308 } 309 } 310 return result.toString(); 311 } 312 313 /** 314 * 从应答中提取 Body 315 * 316 * @param response HTTP 请求应答对象 317 * @return 应答中的Body内容,Body为空时返回空字符串 318 */ 319 public static String extractBody(Response response) { 320 if (response.body() == null) { 321 return ""; 322 } 323 324 try { 325 BufferedSource source = response.body().source(); 326 return source.readUtf8(); 327 } catch (IOException e) { 328 throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); 329 } 330 } 331 332 /** 333 * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 334 * 335 * @param wechatpayPublicKeyId 微信支付公钥ID 336 * @param wechatpayPublicKey 微信支付公钥对象 337 * @param headers 微信支付应答 Header 列表 338 * @param body 微信支付应答 Body 339 */ 340 public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, 341 Headers headers, 342 String body) { 343 String timestamp = headers.get("Wechatpay-Timestamp"); 344 try { 345 Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 346 // 拒绝过期请求 347 if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { 348 throw new IllegalArgumentException( 349 String.format("Validate http response,timestamp[%s] of httpResponse is expires, " 350 + "request-id[%s]", 351 timestamp, headers.get("Request-ID"))); 352 } 353 } catch (DateTimeException | NumberFormatException e) { 354 throw new IllegalArgumentException( 355 String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + 356 "request-id[%s]", timestamp, 357 headers.get("Request-ID"))); 358 } 359 String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), 360 body == null ? "" : body); 361 String serialNumber = headers.get("Wechatpay-Serial"); 362 if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { 363 throw new IllegalArgumentException( 364 String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, 365 serialNumber)); 366 } 367 String signature = headers.get("Wechatpay-Signature"); 368 369 boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); 370 if (!success) { 371 throw new IllegalArgumentException( 372 String.format("Validate response failed,the WechatPay signature is incorrect.%n" 373 + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", 374 headers.get("Request-ID"), headers, body)); 375 } 376 } 377 378 /** 379 * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 380 */ 381 public static class ApiException extends RuntimeException { 382 public final int statusCode; 383 public final String body; 384 public final Headers headers; 385 public ApiException(int statusCode, String body, Headers headers) { 386 super(String.format("微信支付API访问失败,StatusCode: %s, Body: %s", statusCode, body)); 387 this.statusCode = statusCode; 388 this.body = body; 389 this.headers = headers; 390 } 391 } 392}
文档是否有帮助