微信支付SDK JAVA使用方法

使用wechatpay-apache-httpclient,获取httpclient。返回的httpClient将会自动处理请求头中的Authorization鉴权。此处使用的是自动更新证书功能(可选)版本,将自动获取微信平台证书。
sdk github:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient

  1. /**
  2. * 获取访问微信服务器的httpclient
  3. MERCHANTID : 商户号
  4. MERCHANT_SERIAL_NUMBER:商户CA证书序列号,商户后台查看CA证书获取
  5. privateKey:利用工具生成的商户私钥apiclient_key.pem,经过处理后生成PrivateKey对象,再传入
  6. * @return
  7. */
  8. public static HttpClient getWXHttpClient(){
  9. String apiV3Key = WXPayParam.API_V3;
  10. PrivateKey privateKey = getPrivateKey();
  11. AutoUpdateCertificatesVerifier verifier = getVerifier();
  12. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
  13. .withMerchant(WXPayParam.MERCHANTID, WXPayParam.MERCHANT_SERIAL_NUMBER, privateKey)
  14. .withValidator(new WechatPay2Validator(verifier));
  15. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
  16. HttpClient httpClient = builder.build();
  17. return httpClient;
  18. }
  19. /**
  20. * 获取verifier
  21. * @return
  22. */
  23. public static AutoUpdateCertificatesVerifier getVerifier(){
  24. AutoUpdateCertificatesVerifier verifier = null;
  25. PrivateKey privateKey = getPrivateKey();
  26. try {
  27. verifier = new AutoUpdateCertificatesVerifier(
  28. new WechatPay2Credentials(WXPayParam.MERCHANTID, new PrivateKeySigner(WXPayParam.MERCHANT_SERIAL_NUMBER, privateKey)),
  29. WXPayParam.API_V3.getBytes("utf-8"));
  30. } catch (UnsupportedEncodingException e) {
  31. e.printStackTrace();
  32. }
  33. return verifier;
  34. }

请求签名

商户需要使用自身的私钥对API URL、消息体等关键数据的组合进行SHA-256 with RSA签名。请求的签名信息通过HTTP头Authorization传递,具体说明请见签名生成指南。没有携带签名或者签名验证不通过的请求,都不会被执行,并返回401 Unauthorized

其中privateKeygetPrivateKey函数读取CA颁发证书中的apiclient_key.pem后生成,详见下方getPrivateKey函数。
注意:读取pem文件后需要去除开头的-----BEGIN PRIVATE KEY-----和结尾的-----END PRIVATE KEY-----

示例:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCKPgXTnYkgcNK yNHGK9wjKbTCmnG8ikThJG0B9uLt9vbAyJRP4K05fBk/5Lz+33aVAH1pIk5ftD/L 1HCR0XxWijMH0FkCGk/De0Yxrh1cdcL/l0z9NnvsHACkEbvs1yMsG8Fhm/fvDq3I Pvtu3DWXiIZ7hC/wd/AGYJY6hZbGy6X5finZh+mK7s9aBI4gp7xmk7+AoAjJ9zk2 iWTGd9+SgcwiYvK11QhMi+SMMfDiSYkQofm9iumFVexLEldw02jo0nIoziBRP4Yn qePQ3uBWvtK/yjAQ+TryoAtCr/6q7gflZjTtwQxCMGBqEd6k78aAc/WhZko3HLWJ 18hl8YsnAgMBAAECggEALCGDwkhjMgkMioL6q0Bs2NEx9MmF8IS9Ay90V232RoBL taXhkAZRWS+LzaoACy5flZ524t6ZUcd2eK3gqEQlLsZasvv4PzIbzyLF4aThp5Jc sBuDtEoeAJycyK3/OOXtaKkmWzlIMV30wf8OxzPmOnsdlhWFj/Ky68AoZUTX8HcT 7DWpvyA5XBd0BDex89zYK79i50DFFFNLZ7REd5xVG/hD9GDrcaEqkInSm11jxNIX 4Vd07jO53XSxWzJYcA9Xc3+mJqBLbJDLTAN3cv8cLpv4H94WsRxj5Y7gbF71vAcK KdYZ7QuDKsb9swgKxY+1p/6ZOi72Kc7Gz6q4qgmbAQKBgQDmHhtfZMJFKWHrrD8d Gianzzq7EQpCcJQuG5CIxhZVX1bwSJ+B/Gxg1UlDMyr+QHxmOpw73kd2WydDmbNd LNJEDlkm3Efi5QExWT+6TnSKEBgx7oxvAcHATb7/yXtOOwttGkBAC8ljFRKBNW8R

kQFMeelY+kRJZpnzthGWR7KnXwKBgQDX/4SCmxjQKkRLawbHcX1Mib6Bbz84poKr

gCr4ZQHn0rJr/oasn+UB5xlUioW5rLMSAO38d//xW21fjUkxOAwDiGjnpprT4xjM

v9zENU9nXQ8AlF2Jzbha/RkJWvgor7wAhMAm36yW63+d3V7rpBzBHHdXwjblKp34

BhFlLm4ZOQKBgFqCgv+tUOAFG9enYxeePpAIaTBEzoU9ZHsSKnIxf31Kx5Yw6lQl

JbecjHla+dERKhzHdsXxcqgxyCrFnI/MXlOYVSZ8w+WRbzuqv+8Whq37EJkrG59Z

0IxDyBkxdUda3+6kwZqvSCGpmyKpEquVHi6nUMnHfe5k5a6+8QHr52//AoGAN16o

+VII6lPrbenhsv7Ev/oPe96otjz5Aj24xjQeaO76DfURUO8sJXC4bZOU9CPxQ4w5

dZ7NXXGyd+wf9x4G9mDhg4CR7/8nPFVyolmIIVcZoWxnDgxOVgTLhjprowJpjzh4

iX6NH6L+89jrnDxVoqtJbJW8vMJP/GSR0P41+wECgYEAjp85VuTKCiQQE+7epLwZ

MmParo9h2iPZeTYzRl2Ur9FDFFWYJIcTk1ct+opMFju/NLdGtiXMPW+KD/KEiHBK

MTE84Gdd6Ims8lMIFa1MiwLLPmemKfkARNX6yKja83fkPSse8v7DQWvlrnt2JGz7

aBP3r35QWv+BWSZ8wiCky4A=

不知道为什么,利用Scanner读取文件后,自动去除私钥(pem文件内容)所有空格了,因此去除-----BEGINPRIVATEKEY---------ENDPRIVATEKEY-----即可。

  1. /**
  2. * 获取私钥的PrivateKey对象
  3. * @param key
  4. * @return
  5. */
  6. public static PrivateKey getPrivateKey() {
  7. String s = readFile("D:\\_Document\\fanmini\\1353229602_20210310_cert\\apiclient_key.pem");
  8. // System.out.println("===========file:"+s);
  9. return getPrivateKey(s);
  10. }
  11. public static PrivateKey getPrivateKey(String key) {
  12. try {
  13. //这个也行
  14. // byte[] byteKey = new BASE64Decoder().decodeBuffer(key);
  15. byte[] byteKey = DatatypeConverter.parseBase64Binary(key);
  16. PKCS8EncodedKeySpec x509EncodedKeySpec = new PKCS8EncodedKeySpec(byteKey);
  17. KeyFactory keyFactory = KeyFactory.getInstance("RSA");
  18. return keyFactory.generatePrivate(x509EncodedKeySpec);
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. }
  22. return null;
  23. }
  24. /**
  25. * 读取文件
  26. * @param filePath
  27. * @return
  28. */
  29. private static String readFile(String filePath){
  30. File file = new File(filePath);
  31. try(Scanner scanner=new Scanner(file)){
  32. StringBuilder stringBuilder = new StringBuilder();
  33. while(scanner.hasNext()){
  34. stringBuilder.append(scanner.next());
  35. }
  36. String s = stringBuilder.toString();
  37. return s.replace("-----BEGINPRIVATEKEY-----","").replace("-----ENDPRIVATEKEY-----","");
  38. } catch (FileNotFoundException e) {
  39. e.printStackTrace();
  40. return null;
  41. }
  42. }

微信apiV3接口访问示例代码

文档: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml

统一下单

像微信支付平台提交订单,获取prepay_id.

  1. /**
  2. * 统一下单
  3. * orderId:商户订单编号
  4. * content:商品详情
  5. * amount:订单金额
  6. */
  7. public static String putOrder(String orderId, String content, BigDecimal amount) throws UnsupportedEncodingException {
  8. HttpClient httpClient = WXUtil.getWXHttpClient();
  9. int total = amount.multiply(new BigDecimal(100)).intValue();
  10. //配置所需参数
  11. JSONObject jsonObject = new JSONObject();
  12. jsonObject.put("appid",WXPayParam.APPID);
  13. jsonObject.put("mchid",WXPayParam.MERCHANTID);
  14. jsonObject.put("description",content);
  15. jsonObject.put("out_trade_no",orderId);
  16. jsonObject.put("notify_url",WXPayUrl.NOTIFY_URL);
  17. jsonObject.put("amount",new MapObject().put("total",total).put("currency","CNY"));
  18. jsonObject.put("payer",new MapObject().put("openid","ozQGu5ZKdSlDtAvR0yjcthNijmGA"));
  19. //StringEntity作为POST请求中的body
  20. StringEntity stringEntity = new StringEntity(jsonObject.toString());
  21. stringEntity.setContentType(ContentType.APPLICATION_JSON.toString());
  22. // 创建http请求
  23. HttpPost httpPost = new HttpPost(WXPayUrl.PAY);
  24. httpPost.setEntity(stringEntity);
  25. //设置请求头,必须填,可以默认为这两个。
  26. //详见:https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu
  27. httpPost.setHeader("Accept","application/json");
  28. httpPost.setHeader("User-Agent","用户代理(https://zh.wikipedia.org/wiki/User_agent)");
  29. // 后面跟使用Apache HttpClient一样
  30. try {
  31. HttpResponse response = httpClient.execute(httpPost);
  32. HttpEntity entity = response.getEntity();
  33. JSONObject resJson = JSONUtil.parseObj(EntityUtils.toString(entity));
  34. return resJson.get("prepay_id").toString();
  35. } catch (IOException e) {
  36. e.printStackTrace();
  37. }
  38. return "";
  39. }

调起微信小程序支付接口

用途:返回微信支付调用时需要的参数,包括计算签名、prepay_id.

prepayId从上述putOrder函数获取。签名从getMiniProgramSign函数获取,如下所示。
接口参数详见:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml

  1. public static JSONObject callMiniPay(String prepayId){
  2. String timeStamp = String.valueOf( DateUtil.currentSeconds());
  3. String nonceStr = RandomUtil.randomString(32);
  4. String packageStr = "prepay_id=" + prepayId;
  5. String paySign = WXUtil.getMiniProgramSign(WXPayParam.APPID, timeStamp, nonceStr, packageStr);
  6. JSONObject jsonObject = new JSONObject();
  7. jsonObject.put("appId",WXPayParam.APPID);
  8. jsonObject.put("timeStamp",timeStamp);
  9. jsonObject.put("nonceStr",nonceStr);
  10. jsonObject.put("package",packageStr);
  11. jsonObject.put("signType","RSA");
  12. jsonObject.put("paySign",paySign);
  13. return jsonObject;
  14. }

获取小程序调起支付的签名getMiniProgramSign函数:
注意:

签名串一共有四行,每一行为一个参数。行尾以\n(换行符,ASCII编码值为0x0A)结束,包括最后一行。

最后一行一定还要加换行符

  1. /**
  2. * 获取小程序调起支付的签名
  3. * @param appId
  4. * @param timeStamp
  5. * @param nonceStr
  6. * @param packageStr
  7. * @return
  8. */
  9. public static String getMiniProgramSign(String appId,String timeStamp,String nonceStr,String packageStr){
  10. // {"prepay_id":"wx111528217145060d54d6345ff317950000"}
  11. StringBuilder builder = new StringBuilder();
  12. builder.append(appId);
  13. builder.append("\n");
  14. builder.append(timeStamp);
  15. builder.append("\n");
  16. builder.append(nonceStr);
  17. builder.append("\n");
  18. builder.append(packageStr);
  19. builder.append("\n");
  20. String sign = sign(getPrivateKey(), builder.toString());
  21. System.out.println("sign: "+sign);
  22. return sign;
  23. }

接收微信应答

用途:接受微信发回的支付结果。resource部分需要解密才能知道,包含了商户订单。

主要是对应答的解密。下述代码使用decryptToString函数解密,具体见下述代码。
文档:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml

  1. /**
  2. * 接受微信支付订单通知
  3. *
  4. * @param id
  5. * @param mmap
  6. * @return
  7. */
  8. @ApiOperation(value = "接受微信支付订单通知", notes = "接受微信支付订单通知")
  9. @PostMapping("/get/notification")
  10. @ResponseBody
  11. public Object getNotification(@RequestBody String notification) throws GeneralSecurityException, IOException {
  12. System.out.println("notification: "+notification);
  13. JSONObject jsonObject = JSONUtil.parseObj(notification);
  14. byte[] nonce = jsonObject.getByPath("resource.nonce").toString().getBytes(StandardCharsets.UTF_8);
  15. byte[] associatedData = jsonObject.getByPath("resource.associated_data").toString().getBytes(StandardCharsets.UTF_8);
  16. String ciphertext = jsonObject.getByPath("resource.ciphertext").toString();
  17. String res = WXUtil.decryptToString(associatedData, nonce, ciphertext);
  18. //应答的resource对象
  19. JSONObject jb = JSONUtil.parseObj(res);
  20. System.out.println("res:\n"+jb.toStringPretty());
  21. String trade_state = jb.get("trade_state").toString();
  22. String orderId = jb.get("out_trade_no").toString();
  23. if(trade_state.equals("SUCCESS")){
  24. //更新订单状态为已支付
  25. Order order = orderService.selectByPrimaryKey(orderId);
  26. order.setStatus(OrderStatus.HAS_PAYED.getCode());
  27. order.setPayTime(DateUtils.getNowDate());
  28. orderService.updateByPrimaryKeySelective(order);
  29. //处理订单支付后的操作
  30. orderService.doAfterPay(order);
  31. }
  32. return new MapObject().put("code","SUCCESS").put("message","");
  33. }

解密函数decryptToString
(AES_256对称解密算法,使用apiv3秘钥解密)


    /**
     * apiv3秘钥(后台自己设置的那个)
     */
    private static final byte[] aesKey=WXPayParam.API_V3.getBytes(StandardCharsets.UTF_8);

    /**
     * 解密微信应答报文
     * @param associatedData
     * @param nonce
     * @param ciphertext
     * @return
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
            throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }

下面详细描述对通知数据进行解密的流程:

1、用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key; 2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data; 3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;