微信小程序服务端API安全鉴权&统一调用封装

原创 Nick说说前后端 Nick说说前后端 2024年10月03日 10:00 广东

Nick说说前后端

一个专注于互联网金融、移动支付领域的7年老兵,涉及收单、发卡、存款、汇款、转账等业务。一起谈谈技术,品品人生,聊聊日常。

53篇原创内容

公众号

  • 一、序言

  • 二、前置准备

    • 1、获取小程序AppID和AppSecret

    • 2、下载对称加密密钥

    • 3、下载加签私钥

    • 4、下载验签证书

  • 三、加解密封装

    • 1、相关基础类

    • 2、加解密工具类

  • 四、HTTP调用封装

  • 五、微信服务端API网关调用封装

    • 1、基础类

    • 2、属性类和工具类

    • 3、枚举类

    • 4、网关核心调用抽象类

    • 5、网关核心调用业务类

  • 六、测试用例

    • 1、application.yml

    • 2、相关业务类

      • 1) 获取稳定版接口调用凭据

      • 2) 查询小程序域名配置信息

    • 3、WxApiGatewayController

    • 4、测试结果

      • (1) 获取稳定版接口调用凭据测试

      • (2) 查询小程序域名配置信息测试

一、序言

做过小程序开发的朋友都知道,微信开放平台的接口提供了通信鉴权体系,通过数据加密与签名的机制,可以防止数据泄漏与篡改。

开发者可在小程序管理后台API安全模块,为应用配置密钥与公钥,以此来保障开发者应用和微信开放平台交互的安全性。

在小程序管理后台开启api加密后,开发者需要对原API的请求内容加密与签名,同时API的回包内容需要开发者验签与解密。支持的api可参考支持的接口调用:

https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/

今天我们一起来写个简单、易用的微信API网关接口调用封装,涉及到API的加解密、加验签等,让我们专心关注业务开发。


二、前置准备

开始前,我们需要先在管理后台开启API安全模块,具体步骤可参考:安全鉴权模式介绍。

https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/getting_started/signature-verify.html

1、获取小程序AppID和AppSecret

2、下载对称加密密钥

同时我们需要获取对称加密秘钥,这里对称加密密钥类型,我们选择AES256用于数据加解密。

图片

在这里插入图片描述

3、下载加签私钥

这里的非对称加密密钥类型选择RSA,这里的私钥主要是用来对请求数据加签的。

图片

在这里插入图片描述

4、下载验签证书

这里我们需要下载开放平台证书和密钥编号,用于响应数据的验签,如下:

图片

在这里插入图片描述


三、加解密封装

做好前置准备后,我们开始进行封装,具体我们可以参考:微信小程序api签名指南。

1、相关基础类

(1) WxApiGatewayRequest (加密请求数据体)

@Data
public class WxApiGatewayRequest {

 /**
  * 初始向量,为16字节base64字符串(解码后为12字节随机字符串)
  */
 private String iv;
 /**
  * 加密后的密文,使用base64编码
  */
 private String data;
 /**
  * GCM模式输出的认证信息,使用base64编码
  */
 private String authtag;

}

(2) WxApiGatewayResponse(加密响应数据体)

@Data
public class WxApiGatewayResponse {

 /**
  * 初始向量,为16字节base64字符串(解码后为12字节随机字符串)
  */
 private String iv;
 /**
  * 加密后的密文,使用base64编码
  */
 private String data;
 /**
  * GCM模式输出的认证信息,使用base64编码
  */
 private String authtag;
}

备注:微信API网关请求和响应数据体的字段都是一样的。

2、加解密工具类

该工具类是根据微信服务端api的签名指南进行封装的,这里我们加密算法选择熟悉的AES256_GCM,签名算法选择RSAwithSHA256

里面共包含了AES加解密RSA加验签4个核心方法。

import com.xlyj.common.dto.WxApiGatewayRequest;
import com.xlyj.common.vo.WxApiGatewayResponse;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.PSSParameterSpec;
import java.util.Arrays;
import java.util.Base64;

/**
 * 微信API请求和响应加解密、加验签工具类
 * @author Nick Liu
 * @date 2024/7/3
 */
public abstract class WxApiCryptoUtils {

 private static final String AES_ALGORITHM = "AES";
 private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
 private static final int GCM_TAG_LENGTH = 128;

 private static final String RSA_ALGORITHM = "RSA";
 private static final String SIGNATURE_ALGORITHM = "RSASSA-PSS";
 private static final String HASH_ALGORITHM = "SHA-256";
 private static final String MFG_ALGORITHM = "MGF1";

 private static final String CERTIFICATE_TYPE = "X.509";

 private static final Base64.Decoder DECODER = Base64.getDecoder();
 private static final Base64.Encoder ENCODER = Base64.getEncoder();

 /**
  * AES256_GCM 数据加密
  * @param base64AesKey Base64编码AES密钥
  * @param iv           向量IV
  * @param aad          AAD (url_path + app_id + req_timestamp + sn), 中间竖线分隔
  * @param plainText    明文字符串
  * @return 加密后的请求数据
  */
 public static WxApiGatewayRequest encryptByAES(String base64AesKey, String iv, String aad, String plainText) throws Exception {
  byte[] keyAsBytes = DECODER.decode(base64AesKey);
  byte[] ivAsBytes = DECODER.decode(iv);
  byte[] aadAsBytes = aad.getBytes(StandardCharsets.UTF_8);
  byte[] plainTextAsBytes = plainText.getBytes(StandardCharsets.UTF_8);

  // AES256_GCM加密
  Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
  SecretKeySpec keySpec = new SecretKeySpec(keyAsBytes, AES_ALGORITHM);
  GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, ivAsBytes);
  cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
  cipher.updateAAD(aadAsBytes);

  // 前16字节为加密数据,后16字节为授权标识
  byte[] cipherTextAsBytes = cipher.doFinal(plainTextAsBytes);
  byte[] encryptedData = Arrays.copyOfRange(cipherTextAsBytes, 0, cipherTextAsBytes.length - 16);
  byte[] authTag = Arrays.copyOfRange(cipherTextAsBytes, cipherTextAsBytes.length - 16, cipherTextAsBytes.length);

  WxApiGatewayRequest baseRequest = new WxApiGatewayRequest();
  baseRequest.setIv(iv);
  baseRequest.setData(ENCODER.encodeToString(encryptedData));
  baseRequest.setAuthtag(ENCODER.encodeToString(authTag));
  return baseRequest;
 }

 /**
  * AES256_GCM 数据解密
  * @param base64AesKey Base64编码AES密钥
  * @param aad AAD (url_path + app_id + resp_timestamp + sn), 中间竖线分隔
  * @param response 来自微信API网关的响应
  * @return 解密后的请求明文字符串
  * @throws Exception
  */
 public static String decryptByAES(String base64AesKey, String aad, WxApiGatewayResponse response) throws Exception {
  byte[] keyAsBytes = DECODER.decode(base64AesKey);
  byte[] aadAsBytes = aad.getBytes(StandardCharsets.UTF_8);
  byte[] ivAsBytes = DECODER.decode(response.getIv());
  byte[] truncateTextAsBytes = DECODER.decode(response.getData());
  byte[] authTagAsBytes = DECODER.decode(response.getAuthtag());
  byte[] cipherTextAsBytes = new byte[truncateTextAsBytes.length + authTagAsBytes.length];

  // 需要将截断的字节和authTag的字节部分重新组装
  System.arraycopy(truncateTextAsBytes, 0, cipherTextAsBytes, 0, truncateTextAsBytes.length);
  System.arraycopy(authTagAsBytes, 0, cipherTextAsBytes, truncateTextAsBytes.length, authTagAsBytes.length);

  Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
  SecretKeySpec keySpec = new SecretKeySpec(keyAsBytes, AES_ALGORITHM);
  GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, ivAsBytes);
  cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec);
  cipher.updateAAD(aadAsBytes);
  byte[] plainTextAsBytes = cipher.doFinal(cipherTextAsBytes);

  return new String(plainTextAsBytes, StandardCharsets.UTF_8);
 }

 /**
  * RSA with SHA256请求参数加签
  * @param base64PrivateKey Base64编码RSA加签私钥
  * @param payload          请求负载(url_path + app_id + req_timestamp + req_data), 中间换行符分隔
  * @return 签名后的字符串
  */
 public static String signByRSAWithSHA256(String base64PrivateKey, String payload) throws Exception {
  byte[] privateKeyAsBytes = DECODER.decode(base64PrivateKey);
  PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyAsBytes);
  RSAPrivateKey privateKey = (RSAPrivateKey) KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(keySpec);

  Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
  PSSParameterSpec parameterSpec = new PSSParameterSpec(HASH_ALGORITHM, MFG_ALGORITHM, MGF1ParameterSpec.SHA256, 32, 1);
  signature.setParameter(parameterSpec);
  signature.initSign(privateKey);
  signature.update(payload.getBytes(StandardCharsets.UTF_8));
  byte[] signatureAsBytes = signature.sign();
  return ENCODER.encodeToString(signatureAsBytes);
 }

 /**
  * RSA with SHA256响应内容验签
  * @param payload 响应负载(url_path + app_id + resp_timestamp + resp_data)
  * @param base64Certificate 验签证书(Base64编码)
  * @param signature 请求签名
  * @return 是否验签通过
  * @throws Exception
  */
 public static boolean verifySignature(String payload, String base64Certificate, String signature) throws Exception {
  CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
  ByteArrayInputStream inputStream = new ByteArrayInputStream(DECODER.decode(base64Certificate));
  X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);

  Signature verifier = Signature.getInstance(SIGNATURE_ALGORITHM);
  PSSParameterSpec parameterSpec = new PSSParameterSpec(HASH_ALGORITHM, MFG_ALGORITHM, MGF1ParameterSpec.SHA256, 32, 1);
  verifier.setParameter(parameterSpec);
  verifier.initVerify(x509Certificate);
  verifier.update(payload.getBytes(StandardCharsets.UTF_8));

  byte[] signatureInBytes = DECODER.decode(signature);
  return verifier.verify(signatureInBytes);
 }

 /**
  * 生成Base64随机IV
  * @return
  */
 public static String generateRandomIV() {
  byte[] bytes = new byte[12];
  new SecureRandom().nextBytes(bytes);
  return ENCODER.encodeToString(bytes);
 }

 public static String generateNonce(){
  byte[] bytes = new byte[16];
  new SecureRandom().nextBytes(bytes);
  return ENCODER.encodeToString(bytes).replace("=", StringUtils.EMPTY);
 }

}

四、HTTP调用封装

(1) HttpClientProperties

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;

/**
 * @author 刘亚楼
 * @date 2022/5/10
 */
@Data
@ConfigurationProperties(prefix = "http.client")
public class HttpClientProperties {

 /**
  * 连接最大空闲时间
  */
 private Duration maxIdleTime = Duration.ofSeconds(5);

 /**
  * 与服务端建立连接超时时间
  */
 private Duration connectionTimeout = Duration.ofSeconds(5);

 /**
  * 客户端从服务器读取数据超时时间
  */
 private Duration socketTimeout = Duration.ofSeconds(10);

 /**
  * 从连接池获取连接超时时间
  */
 private Duration connectionRequestTimeout = Duration.ofSeconds(3);

 /**
  * 连接池最大连接数
  */
 private int maxTotal = 500;

 /**
  * 每个路由(即ip+端口)最大连接数
  */
 private int defaultMaxPerRoute = 50;

}

(2) HttpClientManager

这个类包含了http请求的封装,如下:

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.AbstractHttpMessage;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Convenient class for http invocation.
 * @author 刘亚楼
 * @date 2022/5/10
 */
public class HttpClientManager {

 private final HttpClient httpClient;

 public HttpClientManager(HttpClient httpClient) {
  this.httpClient = httpClient;
 }

 public HttpClientResp get(String url) throws Exception {
  return this.get(url, Collections.emptyMap(), Collections.emptyMap());
 }

 /**
  * 发送get请求
  * @param url 资源地址
  * @param headers
  * @param params 请求参数
  * @return
  * @throws Exception
  */
 public HttpClientResp get(String url, Map<String, Object> headers, Map<String, Object> params) throws Exception {
  URIBuilder uriBuilder = new URIBuilder(url);
  if (!CollectionUtils.isEmpty(params)) {
   for (Map.Entry<String, Object> param : params.entrySet()) {
    uriBuilder.setParameter(param.getKey(), String.valueOf(param.getValue()));
   }
  }

  HttpGet httpGet = new HttpGet(uriBuilder.build());
  setHeaders(httpGet, headers);
  return getResponse(httpGet);
 }


 /**
  * 模拟表单发送post请求
  * @param url 资源地址
  *
  * @param params 请求参数
  * @return
  * @throws IOException
  */
 public HttpClientResp postInHtmlForm(String url, Map<String, Object> params) throws IOException {
  HttpPost httpPost = new HttpPost(url);
  if (!CollectionUtils.isEmpty(params)) {
   List<NameValuePair> formParams = new ArrayList<>();
   for (Map.Entry<String, Object> param : params.entrySet()) {
    formParams.add(new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue())));
   }
   httpPost.setEntity(new UrlEncodedFormEntity(formParams, Consts.UTF_8));
  }

  return getResponse(httpPost);
 }

 public HttpClientResp postInJson(String url, String jsonStr) throws IOException {
  return this.postInJson(url, Collections.emptyMap(), jsonStr);
 }


 /**
  * 发送post请求,请求参数格式为json
  * @param url 资源地址
  * @param headers 请求头信息
  * @param jsonStr 请求参数json字符串
  * @return
  * @throws IOException
  */
 public HttpClientResp postInJson(String url, Map<String, Object> headers, String jsonStr) throws IOException {
  HttpPost httpPost = new HttpPost(url);
  setHeaders(httpPost, headers);
  httpPost.setEntity(new StringEntity(jsonStr, ContentType.APPLICATION_JSON));
  return getResponse(httpPost);
 }

 public static void setHeaders(AbstractHttpMessage message, Map<String, Object> headers) {
  if (!CollectionUtils.isEmpty(headers)) {
   for (Map.Entry<String, Object> header : headers.entrySet()) {
    message.setHeader(header.getKey(), String.valueOf(header.getValue()));
   }
  }
 }

 private HttpClientResp getResponse(HttpRequestBase request) throws IOException {
  try (CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(request, HttpClientContext.create())) {
   HttpClientResp resp = new HttpClientResp();

   int statusCode = response.getStatusLine().getStatusCode();
   if (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) {
    Map<String, String> headers = new HashMap<>();
    for (Header header : response.getAllHeaders()) {
     headers.put(header.getName(), header.getValue());
    }

    HttpEntity httpEntity = response.getEntity();
    resp.setSuccessful(true);
    resp.setHeaders(headers);
    resp.setContentType(httpEntity.getContentType().getValue());
    resp.setContentLength(httpEntity.getContentLength());
    resp.setRespContent(EntityUtils.toString(httpEntity, Consts.UTF_8));
    if (httpEntity.getContentEncoding() != null) {
     resp.setContentEncoding(httpEntity.getContentEncoding().getValue());
    }
   }

   return resp;
  }

 }

 public static class HttpClientResp {

  private String respContent;
  private long contentLength;
  private String contentType;
  private String contentEncoding;
  private Map<String, String> headers;
  private boolean successful;

  public String getRespContent() {
   return respContent;
  }

  public void setRespContent(String respContent) {
   this.respContent = respContent;
  }

  public long getContentLength() {
   return contentLength;
  }

  public void setContentLength(long contentLength) {
   this.contentLength = contentLength;
  }

  public String getContentType() {
   return contentType;
  }

  public void setContentType(String contentType) {
   this.contentType = contentType;
  }

  public String getContentEncoding() {
   return contentEncoding;
  }

  public void setContentEncoding(String contentEncoding) {
   this.contentEncoding = contentEncoding;
  }

  public Map<String, String> getHeaders() {
   return headers;
  }

  public void setHeaders(Map<String, String> headers) {
   this.headers = headers;
  }

  public boolean isSuccessful() {
   return successful;
  }

  public void setSuccessful(boolean successful) {
   this.successful = successful;
  }

  @Override
  public String toString() {
   return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }

 }

}

五、微信服务端API网关调用封装

1、基础类

(1) WxApiGatewayBaseDTO

该类为请求业务JSON参数基类,里面包含了_n_appid_timestamp三个安全字段。

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;

/**
 * @author Nick Liu
 * @date 2024/7/3
 */
@Data
public class WxApiGatewayBaseDTO {

 /**
  * 安全字段:nonce随机值
  */
 @JSONField(name = "_n")
 private String nonce;

 /**
  * 安全字段:app id
  */
 @JSONField(name = "_appid")
 private String appid;

 /**
  * 安全字段:时间戳
  */
 @JSONField(name = "_timestamp")
 private Long timestamp;

}

(2) WxApiGatewayUrlParamBaseDTO

这里是微信API网关URL参数的基类,这里只定义,没有具体参数。

import lombok.Data;

/**
 * 微信API网关URL参数DTO
 * @author Nick Liu
 * @date 2024/7/27
 */
@Data
public class WxApiGatewayUrlParamBaseDTO {

}

(3) GenericUrlParamsDTO

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GenericUrlParamsDTO extends WxApiGatewayUrlParamBaseDTO {

 @JSONField(name = "access_token")
 private String accessToken;
}

(4) WxApiGatewayErrorMsgVO

这个类包含了微信API网关返回的错误信息,如下:

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;

/**
 * @author Nick Liu
 * @date 2024/8/6
 */
@Data
public class WxApiGatewayErrorMsgVO {

 @JSONField(name = "errcode")
 private Integer errorCode;

 @JSONField(name = "errmsg")
 private String errorMsg;
}

(4) WxApiGatewayBaseVO

这里是微信API网关返回的响应内容基类,当碰到异常时,会返回WxApiGatewayErrorMsgVO类里的错误信息,正常调用会返回该类的信息,如下:

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;

/**
 * @author Nick Liu
 * @date 2024/7/3
 */
@Data
public class WxApiGatewayBaseVO extends WxApiGatewayErrorMsgVO {

 /**
  * 安全字段:nonce随机值
  */
 @JSONField(name = "_n")
 private String nonce;

 /**
  * 安全字段:app id
  */
 @JSONField(name = "_appid")
 private String appid;

 /**
  * 安全字段:时间戳
  */
 @JSONField(name = "_timestamp")
 private long timestamp;
}

2、属性类和工具类

(1) WxApiGatewayProperties

@Data
@Component
@ConfigurationProperties(prefix = "wx.gateway")
public class WxApiGatewayProperties {

 /**
  * 微信网关调用host
  */
 private String host;

 /**
  * 小程序APP ID
  */
 private String appId;

 /**
  * 小程序APP Secret
  */
 private String appSecret;

 /**
  * 对称密钥编号
  */
 private String symmetricSn;

 /**
  * 对称密钥编号
  */
 private String asymmetricSn;
 /**
  * 小程序加密密钥
  */
 private String aesKey;

 /**
  * 小程序加密私钥
  */
 private String privateKey;

 /**
  * 小程序通信验签证书
  */
 private String certificate;
}

(2) FastJsonUtils

/**
 * json字符串与java bean转换工具类
 * @author: liuyalou
 * @date: 2019年10月29日
 */
public class FastJsonUtils {

 public static String toJsonString(Object obj) {
  return toJsonString(obj, null, false, false);
 }

 public static String toJsonString(Object obj, SerializeFilter... filters) {
  return toJsonString(obj, null, false, false, filters);
 }

 public static String toJsonStringWithNullValue(Object obj, SerializeFilter... filters) {
  return toJsonString(obj, null, true, false, filters);
 }

 public static String toPrettyJsonString(Object obj, SerializeFilter... filters) {
  return toJsonString(obj, null, false, true, filters);
 }

 public static String toPrettyJsonStringWithNullValue(Object obj, SerializeFilter... filters) {
  return toJsonString(obj, null, true, true, filters);
 }

 public static String toJsonStringWithDateFormat(Object obj, String dateFormat, SerializeFilter... filters) {
  return toJsonString(obj, dateFormat, false, false, filters);
 }

 public static String toJsonStringWithDateFormatAndNullValue(Object obj, String dateFormat, SerializeFilter... filters) {
  return toJsonString(obj, dateFormat, true, false, filters);
 }

 public static String toPrettyJsonStringWithDateFormat(Object obj, String dateFormat, SerializeFilter... filters) {
  return toJsonString(obj, dateFormat, false, true, filters);
 }

 public static String toPrettyJsonStringWithDateFormatAndNullValue(Object obj, String dateFormat, SerializeFilter... filters) {
  return toJsonString(obj, dateFormat, true, true, filters);
 }

 public static String toJsonString(Object obj, String dateFormat, boolean writeNullValue, boolean prettyFormat, SerializeFilter... filters) {
  if (obj == null) {
   return null;
  }

  int defaultFeature = JSON.DEFAULT_GENERATE_FEATURE;
  if (writeNullValue) {
   return prettyFormat ?
    JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature, SerializerFeature.WriteMapNullValue, SerializerFeature.PrettyFormat) :
    JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature, SerializerFeature.WriteMapNullValue);
  }

  return prettyFormat ?
   JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature, SerializerFeature.PrettyFormat) :
   JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature);

 }

 public static <T> T toJavaBean(String jsonStr, Class<T> clazz) {
  if (StringUtils.isBlank(jsonStr)) {
   return null;
  }

  return JSON.parseObject(jsonStr, clazz);
 }

 public static <T> List<T> toList(String jsonStr, Class<T> clazz) {
  if (StringUtils.isBlank(jsonStr)) {
   return null;
  }

  return JSON.parseArray(jsonStr, clazz);
 }

 public static Map<String, Object> toMap(String jsonStr) {
  if (StringUtils.isBlank(jsonStr)) {
   return null;
  }

  return JSON.parseObject(jsonStr, new TypeReference<Map<String, Object>>() {

  });
 }

 public static Map<String, Integer> toIntegerValMap(String jsonStr) {
  if (StringUtils.isBlank(jsonStr)) {
   return null;
  }

  return JSON.parseObject(jsonStr, new TypeReference<Map<String,Integer>>(){});
 }

 public static Map<String, String> toStringValMap(String jsonStr) {
  if (StringUtils.isBlank(jsonStr)) {
   return null;
  }

  return JSON.parseObject(jsonStr, new TypeReference<Map<String,String>>(){});
 }


 public static Map<String, Object> beanToMap(Object obj) {
  if (Objects.isNull(obj)) {
   return null;
  }

  return toMap(toJsonString(obj));
 }

 public static <T> T mapToJavaBean(Map<String, ? extends Object> map, Class<T> clazz) {
  if (CollectionUtils.isEmpty(map)) {
   return null;
  }

  String jsonStr = JSON.toJSONString(map);
  return JSON.parseObject(jsonStr, clazz);
 }

 /**
  *
  * 对象所有的key,包括嵌套对象的key都会按照自然顺序排序
  * @param obj
  * @return
  */
 public static String toKeyOrderedJsonString(Object obj) {
  return toJsonString(beanToTreeMap(obj));
 }

 /**
  * 对象所有的key按原始顺序排序
  * @param obj
  * @return
  */
 public static String toKeyLinkedJsonString(Object obj) {
  return toJsonString(beanToLinkedHashMap(obj));
 }


 public static Map<String, Object> beanToTreeMap(Object obj) {
  if (Objects.isNull(obj)) {
   return null;
  }

  return toTreeMap(toJsonString(obj));
 }

 public static Map<String, Object> beanToLinkedHashMap(Object obj) {
  if (Objects.isNull(obj)) {
   return null;
  }

  Map<String, Object> linkHashMap = new LinkedHashMap<>();
  Field[] fields = obj.getClass().getDeclaredFields();
  for (Field field : fields) {
   field.setAccessible(true);
   linkHashMap.put(field.getName(), ReflectionUtils.getField(field, obj));
  }
  return linkHashMap;
 }

 public static Map<String, Object> toTreeMap(String jsonStr) {
  if (StringUtils.isBlank(jsonStr)) {
   return null;
  }

  JSONObject jsonObject = JSON.parseObject(jsonStr);
  return convertJsonObjectToMap(jsonObject, TreeMap::new);
 }

 private static Map<String, Object> convertJsonObjectToMap(JSONObject jsonObject, Supplier<Map<String, Object>> supplier) {
  Map<String, Object> map = supplier.get();
  jsonObject.forEach((key, value) -> {
   if (value instanceof JSONObject) {
    // 如果是JSON对象则递归遍历
    map.put(key, convertJsonObjectToMap((JSONObject) value, supplier));
   } else if (value instanceof JSONArray) {
    // 如果是数组则对数组中的元素重新排序
    List<Object> list = new ArrayList<>();
    JSONArray jsonArray = (JSONArray) value;
    jsonArray.forEach(obj -> {
     list.add((obj instanceof JSONObject) ? convertJsonObjectToMap((JSONObject) obj, supplier) : obj);
    });
    map.put(key, list);
   } else {
    // 如果是普通类型则直接赋值
    map.put(key, value);
   }
  });
  return map;
 }

}

3、枚举类

(1) WxApiHeaderEnum

/**
 * Wx API网关调用Header
 * @author Nick Liu
 * @date 2024/7/27
 */
@Getter
public enum WxApiHeaderEnum {

 APP_ID("Wechatmp-Appid", "当前小程序的Appid"),
 TIMESTAMP("Wechatmp-TimeStamp", "时间戳"),
 SERIAL("Wechatmp-Serial", "平台证书编号,在MP管理页面获取,非证书内序列号"),
 SIGNATURE("Wechatmp-Signature", "平台证书签名数据,使用base64编码"),
 ;

 private final String value;
 private final String desc;

 WxApiHeaderEnum(String value, String desc) {
  this.value = value;
  this.desc = desc;
 }

}

(2) WxApiMsgTypeEnum

/**
 * @author Nick Liu
 * @date 2024/7/24
 */
@Getter
public enum WxApiMsgTypeEnum {

 /**
  * 获取稳定版接口调用凭据
  */
 GET_ACCESS_TOKEN("/cgi-bin/stable_token", HttpMethod.POST, false),
 /**
  * 查询每日调用接口的额度,调用次数,频率限制
  */
 GET_API_QUOTA("/cgi-bin/openapi/quota/get", HttpMethod.POST, false),

 /**
  * 查询小程序域名配置信息
  */
 GET_DOMAIN_INFO("/wxa/getwxadevinfo", HttpMethod.POST, true),
 /**
  * 小程序登录
  */
 LOGIN("/cgi-bin/stable_token", HttpMethod.GET, false);
 ;

 /**
  * URL路径
  */
 private final String urlPath;
 /**
  * 支持的HTTP请求方式
  */
 private final HttpMethod httpMethod;
 /**
  * 是否支持安全鉴权,可鉴权的API参考:<a href=https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc>微信Open API</a>
  */
 private final boolean supportSecurityAuth;

 WxApiMsgTypeEnum(String urlPath, HttpMethod httpMethod, boolean supportSecurityAuth) {
  this.urlPath = urlPath;
  this.httpMethod = httpMethod;
  this.supportSecurityAuth = supportSecurityAuth;
 }

 public static WxApiMsgTypeEnum fromUrl(String urlPath) {
  return Arrays.stream(WxApiMsgTypeEnum.values()).filter(e -> e.urlPath.equals(urlPath)).findAny().orElse(null);
 }
}

(3) BizExceptionEnum

@Getter
public enum BizExceptionEnum {
 INVALID_PARAMS("A0101", "Invalid request params"),
 SYSTEM_ERROR("B0001","System exception, please concat customer service"),

 WX_GATEWAY_SYSTEM_ERROR("wx_5000", "WX gateway invocation system error"),
 WX_GATEWAY_BIZ_ERROR("wx_5001", "WX gateway invocation biz error"),
 ;

 private final String code;
 private final String message;

 BizExceptionEnum(String code, String message) {
  this.code = code;
  this.message = message;
 }

 public static BizExceptionEnum fromCode(String code) {
  return Arrays.stream(BizExceptionEnum.values())
   .filter(bizExceptionEnum -> bizExceptionEnum.code.equals(code))
   .findAny()
   .orElse(null);
 }

}

4、网关核心调用抽象类

/**
 * 微信API网关调用封装,包括安全鉴权(加解密,加验签),数据转换等。<br/>
 * 安全鉴权需要在小程序管理后台开启
 * @author Nick Liu
 * @date 2024/7/24
 */
@Slf4j
public abstract class AbstractWxApiGatewayInvocationService {

 private static final String VERTICAL_LINE_SEPARATOR = "|";
 private static final String NEW_LINE_SEPARATOR = "\n";

 @Autowired
 private WxApiGatewayProperties wxApiGatewayProperties;

 @Autowired
 private HttpClientManager httpClientManager;

 /**
  * 预处理请求负载,填充安全字段
  * @param payload
  * @param <T>
  */
 private <T extends WxApiGatewayBaseDTO> void preProcess(T payload) {
  payload.setAppid(wxApiGatewayProperties.getAppId());
  payload.setNonce(WxApiCryptoUtils.generateNonce());
  payload.setTimestamp(DateTimeUtils.getUnixTimestamp());
 }

 /**
  * 请求数据加密
  * @param requestUrl 当前请求API的URL,不包括URL参数(URL Query),需要带HTTP协议头
  * @param payload 请求负载
  * @return 响应内容
  * @param <T> 响应内容参数泛型
  * @throws Exception
  */
 private <T extends WxApiGatewayBaseDTO> WxApiGatewayRequest encryptRequest(String requestUrl, T payload) throws Exception {
  String appId = wxApiGatewayProperties.getAppId();
  String sn = wxApiGatewayProperties.getSymmetricSn();
  String secretKey = wxApiGatewayProperties.getAesKey();
  long timeStamp = payload.getTimestamp();

  List<String> aadParamList = Arrays.asList(requestUrl, appId, String.valueOf(timeStamp), sn);
  String aad = StringUtils.join(aadParamList, VERTICAL_LINE_SEPARATOR);
  String iv = WxApiCryptoUtils.generateRandomIV();
  String plainText = FastJsonUtils.toJsonString(payload);
  return WxApiCryptoUtils.encryptByAES(secretKey, iv, aad, plainText);
 }

 /**
  * 请求签名
  * @param requestUrl 请求URL
   * @param plainPayload 明文请求负载
  * @param cipherPayload 密文请求负载
  * @return Base64签名字符串
  * @param <T> 请求参数泛型
  * @throws Exception
  */
 private <T extends WxApiGatewayBaseDTO> String sign(String requestUrl, T plainPayload, String cipherPayload) throws Exception {
  String appId = wxApiGatewayProperties.getAppId();
  String privateKey = wxApiGatewayProperties.getPrivateKey();
  long timestamp = plainPayload.getTimestamp();

  List<String> signDataList = Arrays.asList(requestUrl, appId, String.valueOf(timestamp), cipherPayload);
  String signData = StringUtils.join(signDataList, NEW_LINE_SEPARATOR);
  return WxApiCryptoUtils.signByRSAWithSHA256(privateKey, signData);
 }

 /**
  * 响应解密
  * @param requestUrl 请求url
  * @param respHeaders 响应头
  * @param resp 加密响应数据
  * @return 解密后的响应报文
  * @throws Exception
  */
 private String decryptResp(String requestUrl, Map<String, String> respHeaders, WxApiGatewayResponse resp) throws Exception {
  String respTimestamp = respHeaders.get(WxApiHeaderEnum.TIMESTAMP.getValue());
  String appId = wxApiGatewayProperties.getAppId();
  String sn = wxApiGatewayProperties.getSymmetricSn();
  String secretKey = wxApiGatewayProperties.getAesKey();

  List<String> aadParamList = Arrays.asList(requestUrl, appId, respTimestamp, sn);
  String aad = StringUtils.join(aadParamList, VERTICAL_LINE_SEPARATOR);
  return WxApiCryptoUtils.decryptByAES(secretKey, aad, resp);
 }

 /**
  * 响应验签
  * @param requestUrl 请求url
  * @param respHeaders 响应头
  * @param resp 加密后的响应数据
  * @return 是否验签通过
  * @throws Exception
  */
 private boolean verifySignature(String requestUrl, Map<String, String> respHeaders, WxApiGatewayResponse resp)
  throws Exception {
  String appId = wxApiGatewayProperties.getAppId();
  String certificate = wxApiGatewayProperties.getCertificate();
  String respTimestamp = respHeaders.get(WxApiHeaderEnum.TIMESTAMP.getValue());
  String respDataStr = FastJsonUtils.toJsonString(resp);
  String signature = respHeaders.get(WxApiHeaderEnum.SIGNATURE.getValue());

  List<String> aadParamList = Arrays.asList(requestUrl, appId, respTimestamp, respDataStr);
  String payload = StringUtils.join(aadParamList, NEW_LINE_SEPARATOR);
  return WxApiCryptoUtils.verifySignature(payload, certificate, signature);
 }

 protected abstract BizException processInvocationException(Exception e);

 /**
  *  发送GET请求到微信API网关
  * @param msgType 消息类型
  * @param urlParams URL参数
  * @param clazz 返回明文Class实例
  * @return 明文响应内容
  * @param <T> 业务请求负载泛型
  * @param <U> 业务请求URL参数泛型
  * @param <R> 业务返回响应泛型
  * @throws Exception
  */
 protected <T extends WxApiGatewayBaseDTO, U extends WxApiGatewayUrlParamBaseDTO, R extends WxApiGatewayBaseVO> R sendGetToWxApiGateway(
  WxApiMsgTypeEnum msgType, U urlParams, Class<R> clazz) {
  try {
   return this.sendRequestToWxApiGateway(msgType, urlParams, null, clazz);
  } catch (Exception e) {
   log.error("微信API网关调用异常: {}", e.getMessage(), e);
   throw this.processInvocationException(e);
  }
 }

 /**
  *  发送POST请求到微信API网关
  * @param msgType 消息类型
  * @param urlParams URL参数
  * @param payload 请求负载: 只有POST请求才有
  * @param clazz 返回明文Class实例
  * @return 明文响应内容
  * @param <T> 业务请求负载泛型
  * @param <U> 业务请求URL参数泛型
  * @param <R> 业务返回响应泛型
  * @throws Exception
  */
 protected <T extends WxApiGatewayBaseDTO, U extends WxApiGatewayUrlParamBaseDTO, R extends WxApiGatewayBaseVO> R sendPostToWxApiGateway(
  WxApiMsgTypeEnum msgType, U urlParams, T payload, Class<R> clazz) {
  try {
   return this.sendRequestToWxApiGateway(msgType, urlParams, payload, clazz);
  } catch (Exception e) {
   log.error("微信API网关调用异常: {}", e.getMessage(), e);
   throw this.processInvocationException(e);
  }
 }


 /**
  *  发送请求到微信API网关
  * @param msgType 消息类型
  * @param urlParams URL参数
  * @param payload 请求负载: 只有POST请求才有
  * @param clazz 返回明文Class实例
  * @return 明文响应内容
  * @param <T> 业务请求负载泛型
  * @param <U> 业务请求URL参数泛型
  * @param <R> 业务返回响应泛型
  * @throws Exception
  */
 private <T extends WxApiGatewayBaseDTO, U extends WxApiGatewayUrlParamBaseDTO, R extends WxApiGatewayBaseVO> R sendRequestToWxApiGateway(
  WxApiMsgTypeEnum msgType, U urlParams, @Nullable T payload, Class<R> clazz) throws Exception {

  // 1、拼接完整的URL
  String host = wxApiGatewayProperties.getHost();
  String urlParamsStr = this.generateUrlParams(urlParams);
  String requestUrl = host + msgType.getUrlPath();
  String fullRequestUrl = requestUrl + urlParamsStr;

  // 2、GET请求不支持安全授权,直接发起网关调用
  if (HttpMethod.GET == msgType.getHttpMethod()) {
   log.info("微信API网关[GET]请求, url: [{}]", requestUrl);
   HttpClientResp httpClientResp = httpClientManager.get(fullRequestUrl);
   String respStr = httpClientResp.getRespContent();
   log.info("微信API网关[GET]响应, url: [{}], 响应内容:{}", requestUrl, respStr);
   R response = FastJsonUtils.toJavaBean(respStr, clazz);
   this.processRespCode(response);
   return response;
  }

  // 3、只有post请求且需要安全验证才验签
  if (HttpMethod.POST == msgType.getHttpMethod() && msgType.isSupportSecurityAuth()) {
   // 参数预处理,填充安全字段
   this.preProcess(payload);

   // 3.2 请求加密
   WxApiGatewayRequest wxApiGatewayRequest = this.encryptRequest(requestUrl, payload);
   String plainReqStr = FastJsonUtils.toJsonString(payload);
   String cipherReqStr = FastJsonUtils.toKeyLinkedJsonString(wxApiGatewayRequest);

   // 3.1 签名
   String signature = this.sign(requestUrl, payload, cipherReqStr);
   Map<String, Object> headers = new HashMap<>();
   headers.put(WxApiHeaderEnum.APP_ID.getValue(), payload.getAppid());
   headers.put(WxApiHeaderEnum.TIMESTAMP.getValue(), payload.getTimestamp());
   headers.put(WxApiHeaderEnum.SIGNATURE.getValue(), signature);
   String headersStr = FastJsonUtils.toJsonString(headers);

   // 3.3 发起网关调用
   log.info("微信API网关[POST]请求, url: [{}], 请求明文:{}", requestUrl, plainReqStr);
   log.info("微信API网关[POST]请求, url: [{}], 请求头:{}, 请求密文:{}", requestUrl, headersStr, cipherReqStr);
   HttpClientResp httpClientResp = httpClientManager.postInJson(fullRequestUrl, headers, cipherReqStr);
   String cipherRespStr = httpClientResp.getRespContent();

   // String respHeaderStr = FastJsonUtils.toJsonString(httpClientResp.getHeaders());
   log.info("微信API网关[POST]响应, url: [{}], 响应密文:{}", requestUrl, cipherRespStr);
   // 响应可能会失败,解密前处理特殊情况
   R response = FastJsonUtils.toJavaBean(cipherRespStr, clazz);
   this.processRespCode(response);

   // 3.4 解密响应报文
   WxApiGatewayResponse cipherResp = FastJsonUtils.toJavaBean(cipherRespStr, WxApiGatewayResponse.class);
   String plainRespStr = this.decryptResp(requestUrl, httpClientResp.getHeaders(), cipherResp);
   log.info("微信API网关[POST]响应, url: [{}], 响应明文:{}", requestUrl, plainRespStr);
   return FastJsonUtils.toJavaBean(plainRespStr, clazz);
  }

  // 4、只需POST请求无需验签
  if (HttpMethod.POST == msgType.getHttpMethod()) {
   String plainRequestStr = FastJsonUtils.toJsonString(payload);
   log.info("微信API网关[POST]请求, url: [{}], 请求明文:{}", requestUrl, plainRequestStr);
   HttpClientResp httpClientResp = httpClientManager.postInJson(fullRequestUrl, plainRequestStr);
   String plainRespStr = httpClientResp.getRespContent();
   log.info("微信API网关[POST]响应, url: [{}], 响应明文:{}", requestUrl, plainRespStr);
   R response = FastJsonUtils.toJavaBean(plainRespStr, clazz);
   this.processRespCode(response);
   return response;
  }

  throw new UnsupportedOperationException("只支持GET或者POST请求");
 }

 private <R extends WxApiGatewayBaseVO> void processRespCode(R response) {
  if (!Objects.isNull(response.getErrorCode()) && WxApiGatewayErrorCode.SUCCESS != response.getErrorCode()) {
   throw new BizException(BizExceptionEnum.WX_GATEWAY_BIZ_ERROR, response.getErrorMsg());
  }
 }

 /**
  * 生成URL参数
  * @param urlParam URL参数实例
  * @return 带?的参数字符串
  * @param <U> URL参数泛型
  * @throws Exception
  */
 private <U extends WxApiGatewayUrlParamBaseDTO> String generateUrlParams(U urlParam) throws Exception {
  if (Objects.isNull(urlParam)) {
   return StringUtils.EMPTY;
  }

  Field[] fields = urlParam.getClass().getDeclaredFields();
  if (ArrayUtils.isEmpty(fields)) {
   return StringUtils.EMPTY;
  }

  StringBuilder urlPramsBuilder = new StringBuilder("?");
  for (Field field : fields) {
   field.setAccessible(true);
   JSONField jsonField = field.getAnnotation(JSONField.class);
   String fieldName = Objects.isNull(jsonField) ? field.getName() : jsonField.name();
   Object fieldValue = field.get(urlParam);
   if (!Objects.isNull(fieldValue)) {
    urlPramsBuilder.append(fieldName).append("=").append(fieldValue).append("&");
   }
  }

  urlPramsBuilder.deleteCharAt(urlPramsBuilder.length() - 1);
  return urlPramsBuilder.toString();
 }

}

5、网关核心调用业务类

/**
 * 微信API网关调用器,指定消息类型,业务请求参数和响应内容类型即可
 * @author Nick Liu
 * @date 2024/7/27
 */
@Slf4j
@Service
public class WxApiGatewayInvoker extends AbstractWxApiGatewayInvocationService {

 @Override
 protected BizException processInvocationException(Exception e) {
  if (e instanceof BizException) {
   throw (BizException) e;
  }
  return new BizException(BizExceptionEnum.WX_GATEWAY_SYSTEM_ERROR);
 }

 /**
  * 获取稳定版本接口调用凭证
  * @param stableAccessTokenDTO 获取稳定版本Token业务参数
  * @return
  */
 public StableAccessTokenVO getStableAccessToken(StableAccessTokenDTO stableAccessTokenDTO) {
  return super.sendPostToWxApiGateway(WxApiMsgTypeEnum.GET_ACCESS_TOKEN, null, stableAccessTokenDTO, StableAccessTokenVO.class);
 }

 /**
  * 查询API调用额度
  * @param genericUrlParamsDTO
  * @param apiQuotaDTO
  * @return
  */
 public ApiQuotaVO getApiQuota(GenericUrlParamsDTO genericUrlParamsDTO, ApiQuotaDTO apiQuotaDTO) {
  return super.sendPostToWxApiGateway(WxApiMsgTypeEnum.GET_API_QUOTA, genericUrlParamsDTO, apiQuotaDTO, ApiQuotaVO.class);
 }

 /**
  * 查询域名配置
  * @param genericUrlParamsDTO
  * @param domainInfoDTO
  * @return
  */
 public DomainInfoVO getDomainInfo(GenericUrlParamsDTO genericUrlParamsDTO, DomainInfoDTO domainInfoDTO){
  return super.sendPostToWxApiGateway(WxApiMsgTypeEnum.GET_DOMAIN_INFO, genericUrlParamsDTO, domainInfoDTO, DomainInfoVO.class);
 }


 /**
  * 小程序登录接口
  * @param miniProgramLoginDTO 小程序登录接口业务参数
  * @return
  */
 public MiniProgramLoginVO login(MiniProgramLoginDTO miniProgramLoginDTO) {
  return super.sendGetToWxApiGateway(WxApiMsgTypeEnum.GET_ACCESS_TOKEN, miniProgramLoginDTO, MiniProgramLoginVO.class);
 }

}

六、测试用例

1、application.yml

# http client configuration
http:
  client:
    max-total: 500 # 连接池最大连接数
    default-max-per-route: 100 # 每个路由最大连接数
    max-idle-time: 5s # 连接最大空闲时间
    connection-request-timeout: 3s # 从连接池获取连接超时时间
    connection-timeout: 5s # 与服务端建立连接超时时间
    socket-timeout: 10s # 客户端从服务器读取数据超时时间
# 微信API网关配置
wx:
  gateway:
    host: https://api.weixin.qq.com
    app-id: appId
    app-secret: appSecret
    # 对称密钥证书编号
    symmetric-sn: xxx
    # 非对称密钥证书编号
    asymmetric-sn: xxx
    # AES秘钥
    aes-key: xxxxxxxxxxxxxxxxxxxxxx
    # 加签私钥
    private-key: xxxxxxxxxxxxxxxxxxxxxx
    # 验签证书
    certificate: xxxxxxxxxxxxxxxxxxxxxx

2、相关业务类

1) 获取稳定版接口调用凭据

(1) StableAccessTokenDTO

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StableAccessTokenDTO extends WxApiGatewayBaseDTO {

 /**
  * 填写固定值 client_credential
  */
 @JSONField(name = "grant_type")
 private String grantType = "client_credential";
 /**
  * 账号唯一凭证,即 AppID
  */
 @JSONField(name = "appid")
 private String appId;
 /**
  * 账号唯一凭证密钥,即 AppSecret
  */
 private String secret;
 /**
  * 默认使用 false。
  * 1. force_refresh = false 时为普通调用模式,access_token 有效期内重复调用该接口不会更新 access_token;
  * 2. 当force_refresh = true 时为强制刷新模式,会导致上次获取的 access_token 失效,并返回新的 access_token
  */
 @JSONField(name = "force_refresh")
 private boolean forceRefresh;
}

(2) StableAccessTokenVO

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StableAccessTokenVO extends WxApiGatewayBaseVO {

 /**
  * 获取到的凭证
  */
 @JSONField(name = "access_token")
 private String accessToken;

 /**
  * 凭证有效时间,单位:秒。目前是7200秒之内的值。
  */
 @JSONField(name = "expires_in")
 private Integer expiresIn;
}

2) 查询小程序域名配置信息

(1) DomainInfoDTO

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DomainInfoDTO extends WxApiGatewayBaseDTO {

 /**
  * 查询配置域名的类型, 可选值如下:
  * 1. getbizdomain 返回业务域名
  * 2. getserverdomain 返回服务器域名
  * 3. 不指明返回全部
  */
 private String action;
}

(2) DomainInfoVO

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DomainInfoVO extends WxApiGatewayBaseVO {

 @JSONField(name = "requestdomain")
 private List<String> requestDomain;
}

3、WxApiGatewayController

@RestController
@RequestMapping("/wx/api")
@RequiredArgsConstructor
public class WxApiGatewayController {

 private final WxApiGatewayInvoker wxApiGatewayInvoker;

 @PostMapping("/access-token/stable")
 public ApiResponse<StableAccessTokenVO> getStableAccessToken(@RequestBody StableAccessTokenDTO stableAccessTokenDTO) {
  return ApiResponse.success(wxApiGatewayInvoker.getStableAccessToken(stableAccessTokenDTO));
 }

 @PostMapping("/domain/info")
 public ApiResponse<DomainInfoVO> getApiQuota(@RequestParam String accessToken, @RequestBody DomainInfoDTO domainInfoDTO) {
  GenericUrlParamsDTO genericUrlParamsDTO = GenericUrlParamsDTO.builder().accessToken(accessToken).build();
  return ApiResponse.success(wxApiGatewayInvoker.getDomainInfo(genericUrlParamsDTO, domainInfoDTO));
 }

}

4、测试结果

(1) 获取稳定版接口调用凭据测试

这个接口不支持安全鉴权,测试结果如下:

图片

获取稳定版接口调用凭据测试结果

(2) 查询小程序域名配置信息测试

这个接口支持安全鉴权,测试结果如下:

图片

查询小程序域名配置信息测试结果

Nick说说前后端

一个专注于互联网金融、移动支付领域的7年老兵,涉及收单、发卡、存款、汇款、转账等业务。一起谈谈技术,品品人生,聊聊日常。

53篇原创内容

公众号

往期推荐

精彩好文等你来

图片

索引与磁盘关系:为何索引能提升查询效率?

图片

一篇文章让你秒懂MySQL中的各种锁

图片

快速了解:MySQL InnoDB和MyISAM的区别

图片

MySQL深度解析:InnoDB内存与磁盘结构全揭秘

图片

前后端RSA互相加解密、加签验签、密钥对生成(Java)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值