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

打印 上一主题 下一主题

主题 815|帖子 815|积分 2445

一、序言

做过小程序开辟的朋友都知道,微信开放平台的接口提供了通信鉴权体系,通过数据加密与签名的机制,可以防止数据泄漏与窜改。
开辟者可在小程序管理后台API安全模块,为应用设置密钥与公钥,以此来保障开辟者应用和微信开放平台交互的安全性。
在小程序管理后台开启api加密后,开辟者须要对原API的请求内容加密与签名,同时API的回包内容须要开辟者验签与解密。支持的api可参考支持的接口调用。
今天我们一起来写个简单、易用的微信API网关接口调用封装,涉及到API的加解密、加验签等,让我们用心关注业务开辟。

二、前置准备

开始前,我们须要先在管理后台开启API安全模块,具体步骤可参考:安全鉴权模式先容。
1、获取小程序AppID和AppSecret

2、下载对称加密密钥

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

3、下载加签私钥

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

4、下载验签证书

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


三、加解密封装

做好前置准备后,我们开始举行封装,具体我们可以参考:微信小程序api签名指南。
1、相干底子类

(1) WxApiGatewayRequest (加密请求数据体)
  1. @Data
  2. public class WxApiGatewayRequest {
  3.         /**
  4.          * 初始向量,为16字节base64字符串(解码后为12字节随机字符串)
  5.          */
  6.         private String iv;
  7.         /**
  8.          * 加密后的密文,使用base64编码
  9.          */
  10.         private String data;
  11.         /**
  12.          * GCM模式输出的认证信息,使用base64编码
  13.          */
  14.         private String authtag;
  15. }
复制代码
(2) WxApiGatewayResponse(加密响应数据体)
  1. @Data
  2. public class WxApiGatewayResponse {
  3.         /**
  4.          * 初始向量,为16字节base64字符串(解码后为12字节随机字符串)
  5.          */
  6.         private String iv;
  7.         /**
  8.          * 加密后的密文,使用base64编码
  9.          */
  10.         private String data;
  11.         /**
  12.          * GCM模式输出的认证信息,使用base64编码
  13.          */
  14.         private String authtag;
  15. }
复制代码
  备注:微信API网关请求和响应数据体的字段都是一样的。
  2、加解密工具类

该工具类是根据微信服务端api的签名指南举行封装的,这里我们加密算法选择认识的AES256_GCM,签名算法选择RSAwithSHA256。
里面共包罗了AES加解密RSA加验签4个核心方法。
  1. import com.xlyj.common.dto.WxApiGatewayRequest;
  2. import com.xlyj.common.vo.WxApiGatewayResponse;
  3. import org.apache.commons.lang3.StringUtils;
  4. import javax.crypto.Cipher;
  5. import javax.crypto.spec.GCMParameterSpec;
  6. import javax.crypto.spec.SecretKeySpec;
  7. import java.io.ByteArrayInputStream;
  8. import java.nio.charset.StandardCharsets;
  9. import java.security.KeyFactory;
  10. import java.security.SecureRandom;
  11. import java.security.Signature;
  12. import java.security.cert.CertificateFactory;
  13. import java.security.cert.X509Certificate;
  14. import java.security.interfaces.RSAPrivateKey;
  15. import java.security.spec.MGF1ParameterSpec;
  16. import java.security.spec.PKCS8EncodedKeySpec;
  17. import java.security.spec.PSSParameterSpec;
  18. import java.util.Arrays;
  19. import java.util.Base64;
  20. /**
  21. * 微信API请求和响应加解密、加验签工具类
  22. * @author Nick Liu
  23. * @date 2024/7/3
  24. */
  25. public abstract class WxApiCryptoUtils {
  26.         private static final String AES_ALGORITHM = "AES";
  27.         private static final String AES_TRANSFORMATION = "AES/GCM/NoPadding";
  28.         private static final int GCM_TAG_LENGTH = 128;
  29.         private static final String RSA_ALGORITHM = "RSA";
  30.         private static final String SIGNATURE_ALGORITHM = "RSASSA-PSS";
  31.         private static final String HASH_ALGORITHM = "SHA-256";
  32.         private static final String MFG_ALGORITHM = "MGF1";
  33.         private static final String CERTIFICATE_TYPE = "X.509";
  34.         private static final Base64.Decoder DECODER = Base64.getDecoder();
  35.         private static final Base64.Encoder ENCODER = Base64.getEncoder();
  36.         /**
  37.          * AES256_GCM 数据加密
  38.          * @param base64AesKey Base64编码AES密钥
  39.          * @param iv           向量IV
  40.          * @param aad          AAD (url_path + app_id + req_timestamp + sn), 中间竖线分隔
  41.          * @param plainText    明文字符串
  42.          * @return 加密后的请求数据
  43.          */
  44.         public static WxApiGatewayRequest encryptByAES(String base64AesKey, String iv, String aad, String plainText) throws Exception {
  45.                 byte[] keyAsBytes = DECODER.decode(base64AesKey);
  46.                 byte[] ivAsBytes = DECODER.decode(iv);
  47.                 byte[] aadAsBytes = aad.getBytes(StandardCharsets.UTF_8);
  48.                 byte[] plainTextAsBytes = plainText.getBytes(StandardCharsets.UTF_8);
  49.                 // AES256_GCM加密
  50.                 Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
  51.                 SecretKeySpec keySpec = new SecretKeySpec(keyAsBytes, AES_ALGORITHM);
  52.                 GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, ivAsBytes);
  53.                 cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
  54.                 cipher.updateAAD(aadAsBytes);
  55.                 // 前16字节为加密数据,后16字节为授权标识
  56.                 byte[] cipherTextAsBytes = cipher.doFinal(plainTextAsBytes);
  57.                 byte[] encryptedData = Arrays.copyOfRange(cipherTextAsBytes, 0, cipherTextAsBytes.length - 16);
  58.                 byte[] authTag = Arrays.copyOfRange(cipherTextAsBytes, cipherTextAsBytes.length - 16, cipherTextAsBytes.length);
  59.                 WxApiGatewayRequest baseRequest = new WxApiGatewayRequest();
  60.                 baseRequest.setIv(iv);
  61.                 baseRequest.setData(ENCODER.encodeToString(encryptedData));
  62.                 baseRequest.setAuthtag(ENCODER.encodeToString(authTag));
  63.                 return baseRequest;
  64.         }
  65.         /**
  66.          * AES256_GCM 数据解密
  67.          * @param base64AesKey Base64编码AES密钥
  68.          * @param aad AAD (url_path + app_id + resp_timestamp + sn), 中间竖线分隔
  69.          * @param response 来自微信API网关的响应
  70.          * @return 解密后的请求明文字符串
  71.          * @throws Exception
  72.          */
  73.         public static String decryptByAES(String base64AesKey, String aad, WxApiGatewayResponse response) throws Exception {
  74.                 byte[] keyAsBytes = DECODER.decode(base64AesKey);
  75.                 byte[] aadAsBytes = aad.getBytes(StandardCharsets.UTF_8);
  76.                 byte[] ivAsBytes = DECODER.decode(response.getIv());
  77.                 byte[] truncateTextAsBytes = DECODER.decode(response.getData());
  78.                 byte[] authTagAsBytes = DECODER.decode(response.getAuthtag());
  79.                 byte[] cipherTextAsBytes = new byte[truncateTextAsBytes.length + authTagAsBytes.length];
  80.                 // 需要将截断的字节和authTag的字节部分重新组装
  81.                 System.arraycopy(truncateTextAsBytes, 0, cipherTextAsBytes, 0, truncateTextAsBytes.length);
  82.                 System.arraycopy(authTagAsBytes, 0, cipherTextAsBytes, truncateTextAsBytes.length, authTagAsBytes.length);
  83.                 Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
  84.                 SecretKeySpec keySpec = new SecretKeySpec(keyAsBytes, AES_ALGORITHM);
  85.                 GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, ivAsBytes);
  86.                 cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec);
  87.                 cipher.updateAAD(aadAsBytes);
  88.                 byte[] plainTextAsBytes = cipher.doFinal(cipherTextAsBytes);
  89.                 return new String(plainTextAsBytes, StandardCharsets.UTF_8);
  90.         }
  91.         /**
  92.          * RSA with SHA256请求参数加签
  93.          * @param base64PrivateKey Base64编码RSA加签私钥
  94.          * @param payload          请求负载(url_path + app_id + req_timestamp + req_data), 中间换行符分隔
  95.          * @return 签名后的字符串
  96.          */
  97.         public static String signByRSAWithSHA256(String base64PrivateKey, String payload) throws Exception {
  98.                 byte[] privateKeyAsBytes = DECODER.decode(base64PrivateKey);
  99.                 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyAsBytes);
  100.                 RSAPrivateKey privateKey = (RSAPrivateKey) KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(keySpec);
  101.                 Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
  102.                 PSSParameterSpec parameterSpec = new PSSParameterSpec(HASH_ALGORITHM, MFG_ALGORITHM, MGF1ParameterSpec.SHA256, 32, 1);
  103.                 signature.setParameter(parameterSpec);
  104.                 signature.initSign(privateKey);
  105.                 signature.update(payload.getBytes(StandardCharsets.UTF_8));
  106.                 byte[] signatureAsBytes = signature.sign();
  107.                 return ENCODER.encodeToString(signatureAsBytes);
  108.         }
  109.         /**
  110.          * RSA with SHA256响应内容验签
  111.          * @param payload 响应负载(url_path + app_id + resp_timestamp + resp_data)
  112.          * @param base64Certificate 验签证书(Base64编码)
  113.          * @param signature 请求签名
  114.          * @return 是否验签通过
  115.          * @throws Exception
  116.          */
  117.         public static boolean verifySignature(String payload, String base64Certificate, String signature) throws Exception {
  118.                 CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
  119.                 ByteArrayInputStream inputStream = new ByteArrayInputStream(DECODER.decode(base64Certificate));
  120.                 X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
  121.                 Signature verifier = Signature.getInstance(SIGNATURE_ALGORITHM);
  122.                 PSSParameterSpec parameterSpec = new PSSParameterSpec(HASH_ALGORITHM, MFG_ALGORITHM, MGF1ParameterSpec.SHA256, 32, 1);
  123.                 verifier.setParameter(parameterSpec);
  124.                 verifier.initVerify(x509Certificate);
  125.                 verifier.update(payload.getBytes(StandardCharsets.UTF_8));
  126.                 byte[] signatureInBytes = DECODER.decode(signature);
  127.                 return verifier.verify(signatureInBytes);
  128.         }
  129.         /**
  130.          * 生成Base64随机IV
  131.          * @return
  132.          */
  133.         public static String generateRandomIV() {
  134.                 byte[] bytes = new byte[12];
  135.                 new SecureRandom().nextBytes(bytes);
  136.                 return ENCODER.encodeToString(bytes);
  137.         }
  138.         public static String generateNonce(){
  139.                 byte[] bytes = new byte[16];
  140.                 new SecureRandom().nextBytes(bytes);
  141.                 return ENCODER.encodeToString(bytes).replace("=", StringUtils.EMPTY);
  142.         }
  143. }
复制代码

四、HTTP调用封装

(1) HttpClientProperties
  1. import lombok.Data;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import java.time.Duration;
  4. /**
  5. * @author 刘亚楼
  6. * @date 2022/5/10
  7. */
  8. @Data
  9. @ConfigurationProperties(prefix = "http.client")
  10. public class HttpClientProperties {
  11.         /**
  12.          * 连接最大空闲时间
  13.          */
  14.         private Duration maxIdleTime = Duration.ofSeconds(5);
  15.         /**
  16.          * 与服务端建立连接超时时间
  17.          */
  18.         private Duration connectionTimeout = Duration.ofSeconds(5);
  19.         /**
  20.          * 客户端从服务器读取数据超时时间
  21.          */
  22.         private Duration socketTimeout = Duration.ofSeconds(10);
  23.         /**
  24.          * 从连接池获取连接超时时间
  25.          */
  26.         private Duration connectionRequestTimeout = Duration.ofSeconds(3);
  27.         /**
  28.          * 连接池最大连接数
  29.          */
  30.         private int maxTotal = 500;
  31.         /**
  32.          * 每个路由(即ip+端口)最大连接数
  33.          */
  34.         private int defaultMaxPerRoute = 50;
  35. }
复制代码
(2) HttpClientManager
这个类包罗了http请求的封装,如下:
  1. import org.apache.commons.lang3.builder.ToStringBuilder;
  2. import org.apache.commons.lang3.builder.ToStringStyle;
  3. import org.apache.http.Consts;
  4. import org.apache.http.Header;
  5. import org.apache.http.HttpEntity;
  6. import org.apache.http.HttpStatus;
  7. import org.apache.http.NameValuePair;
  8. import org.apache.http.client.HttpClient;
  9. import org.apache.http.client.entity.UrlEncodedFormEntity;
  10. import org.apache.http.client.methods.CloseableHttpResponse;
  11. import org.apache.http.client.methods.HttpGet;
  12. import org.apache.http.client.methods.HttpPost;
  13. import org.apache.http.client.methods.HttpRequestBase;
  14. import org.apache.http.client.protocol.HttpClientContext;
  15. import org.apache.http.client.utils.URIBuilder;
  16. import org.apache.http.entity.ContentType;
  17. import org.apache.http.entity.StringEntity;
  18. import org.apache.http.message.AbstractHttpMessage;
  19. import org.apache.http.message.BasicNameValuePair;
  20. import org.apache.http.util.EntityUtils;
  21. import org.springframework.util.CollectionUtils;
  22. import java.io.IOException;
  23. import java.util.ArrayList;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.List;
  27. import java.util.Map;
  28. /**
  29. * Convenient class for http invocation.
  30. * @author 刘亚楼
  31. * @date 2022/5/10
  32. */
  33. public class HttpClientManager {
  34.         private final HttpClient httpClient;
  35.         public HttpClientManager(HttpClient httpClient) {
  36.                 this.httpClient = httpClient;
  37.         }
  38.         public HttpClientResp get(String url) throws Exception {
  39.                 return this.get(url, Collections.emptyMap(), Collections.emptyMap());
  40.         }
  41.         /**
  42.          * 发送get请求
  43.          * @param url 资源地址
  44.          * @param headers
  45.          * @param params 请求参数
  46.          * @return
  47.          * @throws Exception
  48.          */
  49.         public HttpClientResp get(String url, Map<String, Object> headers, Map<String, Object> params) throws Exception {
  50.                 URIBuilder uriBuilder = new URIBuilder(url);
  51.                 if (!CollectionUtils.isEmpty(params)) {
  52.                         for (Map.Entry<String, Object> param : params.entrySet()) {
  53.                                 uriBuilder.setParameter(param.getKey(), String.valueOf(param.getValue()));
  54.                         }
  55.                 }
  56.                 HttpGet httpGet = new HttpGet(uriBuilder.build());
  57.                 setHeaders(httpGet, headers);
  58.                 return getResponse(httpGet);
  59.         }
  60.         /**
  61.          * 模拟表单发送post请求
  62.          * @param url 资源地址
  63.          *
  64.          * @param params 请求参数
  65.          * @return
  66.          * @throws IOException
  67.          */
  68.         public HttpClientResp postInHtmlForm(String url, Map<String, Object> params) throws IOException {
  69.                 HttpPost httpPost = new HttpPost(url);
  70.                 if (!CollectionUtils.isEmpty(params)) {
  71.                         List<NameValuePair> formParams = new ArrayList<>();
  72.                         for (Map.Entry<String, Object> param : params.entrySet()) {
  73.                                 formParams.add(new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue())));
  74.                         }
  75.                         httpPost.setEntity(new UrlEncodedFormEntity(formParams, Consts.UTF_8));
  76.                 }
  77.                 return getResponse(httpPost);
  78.         }
  79.         public HttpClientResp postInJson(String url, String jsonStr) throws IOException {
  80.                 return this.postInJson(url, Collections.emptyMap(), jsonStr);
  81.         }
  82.         /**
  83.          * 发送post请求,请求参数格式为json
  84.          * @param url 资源地址
  85.          * @param headers 请求头信息
  86.          * @param jsonStr 请求参数json字符串
  87.          * @return
  88.          * @throws IOException
  89.          */
  90.         public HttpClientResp postInJson(String url, Map<String, Object> headers, String jsonStr) throws IOException {
  91.                 HttpPost httpPost = new HttpPost(url);
  92.                 setHeaders(httpPost, headers);
  93.                 httpPost.setEntity(new StringEntity(jsonStr, ContentType.APPLICATION_JSON));
  94.                 return getResponse(httpPost);
  95.         }
  96.         public static void setHeaders(AbstractHttpMessage message, Map<String, Object> headers) {
  97.                 if (!CollectionUtils.isEmpty(headers)) {
  98.                         for (Map.Entry<String, Object> header : headers.entrySet()) {
  99.                                 message.setHeader(header.getKey(), String.valueOf(header.getValue()));
  100.                         }
  101.                 }
  102.         }
  103.         private HttpClientResp getResponse(HttpRequestBase request) throws IOException {
  104.                 try (CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(request, HttpClientContext.create())) {
  105.                         HttpClientResp resp = new HttpClientResp();
  106.                         int statusCode = response.getStatusLine().getStatusCode();
  107.                         if (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) {
  108.                                 Map<String, String> headers = new HashMap<>();
  109.                                 for (Header header : response.getAllHeaders()) {
  110.                                         headers.put(header.getName(), header.getValue());
  111.                                 }
  112.                                 HttpEntity httpEntity = response.getEntity();
  113.                                 resp.setSuccessful(true);
  114.                                 resp.setHeaders(headers);
  115.                                 resp.setContentType(httpEntity.getContentType().getValue());
  116.                                 resp.setContentLength(httpEntity.getContentLength());
  117.                                 resp.setRespContent(EntityUtils.toString(httpEntity, Consts.UTF_8));
  118.                                 if (httpEntity.getContentEncoding() != null) {
  119.                                         resp.setContentEncoding(httpEntity.getContentEncoding().getValue());
  120.                                 }
  121.                         }
  122.                         return resp;
  123.                 }
  124.         }
  125.         public static class HttpClientResp {
  126.                 private String respContent;
  127.                 private long contentLength;
  128.                 private String contentType;
  129.                 private String contentEncoding;
  130.                 private Map<String, String> headers;
  131.                 private boolean successful;
  132.                 public String getRespContent() {
  133.                         return respContent;
  134.                 }
  135.                 public void setRespContent(String respContent) {
  136.                         this.respContent = respContent;
  137.                 }
  138.                 public long getContentLength() {
  139.                         return contentLength;
  140.                 }
  141.                 public void setContentLength(long contentLength) {
  142.                         this.contentLength = contentLength;
  143.                 }
  144.                 public String getContentType() {
  145.                         return contentType;
  146.                 }
  147.                 public void setContentType(String contentType) {
  148.                         this.contentType = contentType;
  149.                 }
  150.                 public String getContentEncoding() {
  151.                         return contentEncoding;
  152.                 }
  153.                 public void setContentEncoding(String contentEncoding) {
  154.                         this.contentEncoding = contentEncoding;
  155.                 }
  156.                 public Map<String, String> getHeaders() {
  157.                         return headers;
  158.                 }
  159.                 public void setHeaders(Map<String, String> headers) {
  160.                         this.headers = headers;
  161.                 }
  162.                 public boolean isSuccessful() {
  163.                         return successful;
  164.                 }
  165.                 public void setSuccessful(boolean successful) {
  166.                         this.successful = successful;
  167.                 }
  168.                 @Override
  169.                 public String toString() {
  170.                         return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  171.                 }
  172.         }
  173. }
复制代码

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

1、底子类

(1) WxApiGatewayBaseDTO
该类为请求业务JSON参数基类,里面包罗了_n、_appid、_timestamp三个安全字段。
  1. import com.alibaba.fastjson.annotation.JSONField;
  2. import lombok.Data;
  3. /**
  4. * @author Nick Liu
  5. * @date 2024/7/3
  6. */
  7. @Data
  8. public class WxApiGatewayBaseDTO {
  9.         /**
  10.          * 安全字段:nonce随机值
  11.          */
  12.         @JSONField(name = "_n")
  13.         private String nonce;
  14.         /**
  15.          * 安全字段:app id
  16.          */
  17.         @JSONField(name = "_appid")
  18.         private String appid;
  19.         /**
  20.          * 安全字段:时间戳
  21.          */
  22.         @JSONField(name = "_timestamp")
  23.         private Long timestamp;
  24. }
复制代码
(2) WxApiGatewayUrlParamBaseDTO
这里是微信API网关URL参数的基类,这里只界说,没有具体参数。
  1. import lombok.Data;
  2. /**
  3. * 微信API网关URL参数DTO
  4. * @author Nick Liu
  5. * @date 2024/7/27
  6. */
  7. @Data
  8. public class WxApiGatewayUrlParamBaseDTO {
  9. }
复制代码
(3) GenericUrlParamsDTO
  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class GenericUrlParamsDTO extends WxApiGatewayUrlParamBaseDTO {
  6.         @JSONField(name = "access_token")
  7.         private String accessToken;
  8. }
复制代码
(4) WxApiGatewayErrorMsgVO
这个类包罗了微信API网关返回的错误信息,如下:
  1. import com.alibaba.fastjson.annotation.JSONField;
  2. import lombok.Data;
  3. /**
  4. * @author Nick Liu
  5. * @date 2024/8/6
  6. */
  7. @Data
  8. public class WxApiGatewayErrorMsgVO {
  9.         @JSONField(name = "errcode")
  10.         private Integer errorCode;
  11.         @JSONField(name = "errmsg")
  12.         private String errorMsg;
  13. }
复制代码
(4) WxApiGatewayBaseVO
这里是微信API网关返回的响应内容基类,当碰到非常时,会返回WxApiGatewayErrorMsgVO类里的错误信息,正常调用会返回该类的信息,如下:
  1. import com.alibaba.fastjson.annotation.JSONField;
  2. import lombok.Data;
  3. /**
  4. * @author Nick Liu
  5. * @date 2024/7/3
  6. */
  7. @Data
  8. public class WxApiGatewayBaseVO extends WxApiGatewayErrorMsgVO {
  9.         /**
  10.          * 安全字段:nonce随机值
  11.          */
  12.         @JSONField(name = "_n")
  13.         private String nonce;
  14.         /**
  15.          * 安全字段:app id
  16.          */
  17.         @JSONField(name = "_appid")
  18.         private String appid;
  19.         /**
  20.          * 安全字段:时间戳
  21.          */
  22.         @JSONField(name = "_timestamp")
  23.         private long timestamp;
  24. }
复制代码
2、属性类和工具类

(1) WxApiGatewayProperties
  1. @Data
  2. @Component
  3. @ConfigurationProperties(prefix = "wx.gateway")
  4. public class WxApiGatewayProperties {
  5.         /**
  6.          * 微信网关调用host
  7.          */
  8.         private String host;
  9.         /**
  10.          * 小程序APP ID
  11.          */
  12.         private String appId;
  13.         /**
  14.          * 小程序APP Secret
  15.          */
  16.         private String appSecret;
  17.         /**
  18.          * 对称密钥编号
  19.          */
  20.         private String symmetricSn;
  21.         /**
  22.          * 对称密钥编号
  23.          */
  24.         private String asymmetricSn;
  25.         /**
  26.          * 小程序加密密钥
  27.          */
  28.         private String aesKey;
  29.         /**
  30.          * 小程序加密私钥
  31.          */
  32.         private String privateKey;
  33.         /**
  34.          * 小程序通信验签证书
  35.          */
  36.         private String certificate;
  37. }
复制代码
(2) FastJsonUtils
  1. /**
  2. * json字符串与java bean转换工具类
  3. * @author: liuyalou
  4. * @date: 2019年10月29日
  5. */
  6. public class FastJsonUtils {
  7.         public static String toJsonString(Object obj) {
  8.                 return toJsonString(obj, null, false, false);
  9.         }
  10.         public static String toJsonString(Object obj, SerializeFilter... filters) {
  11.                 return toJsonString(obj, null, false, false, filters);
  12.         }
  13.         public static String toJsonStringWithNullValue(Object obj, SerializeFilter... filters) {
  14.                 return toJsonString(obj, null, true, false, filters);
  15.         }
  16.         public static String toPrettyJsonString(Object obj, SerializeFilter... filters) {
  17.                 return toJsonString(obj, null, false, true, filters);
  18.         }
  19.         public static String toPrettyJsonStringWithNullValue(Object obj, SerializeFilter... filters) {
  20.                 return toJsonString(obj, null, true, true, filters);
  21.         }
  22.         public static String toJsonStringWithDateFormat(Object obj, String dateFormat, SerializeFilter... filters) {
  23.                 return toJsonString(obj, dateFormat, false, false, filters);
  24.         }
  25.         public static String toJsonStringWithDateFormatAndNullValue(Object obj, String dateFormat, SerializeFilter... filters) {
  26.                 return toJsonString(obj, dateFormat, true, false, filters);
  27.         }
  28.         public static String toPrettyJsonStringWithDateFormat(Object obj, String dateFormat, SerializeFilter... filters) {
  29.                 return toJsonString(obj, dateFormat, false, true, filters);
  30.         }
  31.         public static String toPrettyJsonStringWithDateFormatAndNullValue(Object obj, String dateFormat, SerializeFilter... filters) {
  32.                 return toJsonString(obj, dateFormat, true, true, filters);
  33.         }
  34.         public static String toJsonString(Object obj, String dateFormat, boolean writeNullValue, boolean prettyFormat, SerializeFilter... filters) {
  35.                 if (obj == null) {
  36.                         return null;
  37.                 }
  38.                 int defaultFeature = JSON.DEFAULT_GENERATE_FEATURE;
  39.                 if (writeNullValue) {
  40.                         return prettyFormat ?
  41.                                 JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature, SerializerFeature.WriteMapNullValue, SerializerFeature.PrettyFormat) :
  42.                                 JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature, SerializerFeature.WriteMapNullValue);
  43.                 }
  44.                 return prettyFormat ?
  45.                         JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature, SerializerFeature.PrettyFormat) :
  46.                         JSON.toJSONString(obj, SerializeConfig.globalInstance, filters, dateFormat, defaultFeature);
  47.         }
  48.         public static <T> T toJavaBean(String jsonStr, Class<T> clazz) {
  49.                 if (StringUtils.isBlank(jsonStr)) {
  50.                         return null;
  51.                 }
  52.                 return JSON.parseObject(jsonStr, clazz);
  53.         }
  54.         public static <T> List<T> toList(String jsonStr, Class<T> clazz) {
  55.                 if (StringUtils.isBlank(jsonStr)) {
  56.                         return null;
  57.                 }
  58.                 return JSON.parseArray(jsonStr, clazz);
  59.         }
  60.         public static Map<String, Object> toMap(String jsonStr) {
  61.                 if (StringUtils.isBlank(jsonStr)) {
  62.                         return null;
  63.                 }
  64.                 return JSON.parseObject(jsonStr, new TypeReference<Map<String, Object>>() {
  65.                 });
  66.         }
  67.         public static Map<String, Integer> toIntegerValMap(String jsonStr) {
  68.                 if (StringUtils.isBlank(jsonStr)) {
  69.                         return null;
  70.                 }
  71.                 return JSON.parseObject(jsonStr, new TypeReference<Map<String,Integer>>(){});
  72.         }
  73.         public static Map<String, String> toStringValMap(String jsonStr) {
  74.                 if (StringUtils.isBlank(jsonStr)) {
  75.                         return null;
  76.                 }
  77.                 return JSON.parseObject(jsonStr, new TypeReference<Map<String,String>>(){});
  78.         }
  79.         public static Map<String, Object> beanToMap(Object obj) {
  80.                 if (Objects.isNull(obj)) {
  81.                         return null;
  82.                 }
  83.                 return toMap(toJsonString(obj));
  84.         }
  85.         public static <T> T mapToJavaBean(Map<String, ? extends Object> map, Class<T> clazz) {
  86.                 if (CollectionUtils.isEmpty(map)) {
  87.                         return null;
  88.                 }
  89.                 String jsonStr = JSON.toJSONString(map);
  90.                 return JSON.parseObject(jsonStr, clazz);
  91.         }
  92.         /**
  93.          *
  94.          * 对象所有的key,包括嵌套对象的key都会按照自然顺序排序
  95.          * @param obj
  96.          * @return
  97.          */
  98.         public static String toKeyOrderedJsonString(Object obj) {
  99.                 return toJsonString(beanToTreeMap(obj));
  100.         }
  101.         /**
  102.          * 对象所有的key按原始顺序排序
  103.          * @param obj
  104.          * @return
  105.          */
  106.         public static String toKeyLinkedJsonString(Object obj) {
  107.                 return toJsonString(beanToLinkedHashMap(obj));
  108.         }
  109.         public static Map<String, Object> beanToTreeMap(Object obj) {
  110.                 if (Objects.isNull(obj)) {
  111.                         return null;
  112.                 }
  113.                 return toTreeMap(toJsonString(obj));
  114.         }
  115.         public static Map<String, Object> beanToLinkedHashMap(Object obj) {
  116.                 if (Objects.isNull(obj)) {
  117.                         return null;
  118.                 }
  119.                 Map<String, Object> linkHashMap = new LinkedHashMap<>();
  120.                 Field[] fields = obj.getClass().getDeclaredFields();
  121.                 for (Field field : fields) {
  122.                         field.setAccessible(true);
  123.                         linkHashMap.put(field.getName(), ReflectionUtils.getField(field, obj));
  124.                 }
  125.                 return linkHashMap;
  126.         }
  127.         public static Map<String, Object> toTreeMap(String jsonStr) {
  128.                 if (StringUtils.isBlank(jsonStr)) {
  129.                         return null;
  130.                 }
  131.                 JSONObject jsonObject = JSON.parseObject(jsonStr);
  132.                 return convertJsonObjectToMap(jsonObject, TreeMap::new);
  133.         }
  134.         private static Map<String, Object> convertJsonObjectToMap(JSONObject jsonObject, Supplier<Map<String, Object>> supplier) {
  135.                 Map<String, Object> map = supplier.get();
  136.                 jsonObject.forEach((key, value) -> {
  137.                         if (value instanceof JSONObject) {
  138.                                 // 如果是JSON对象则递归遍历
  139.                                 map.put(key, convertJsonObjectToMap((JSONObject) value, supplier));
  140.                         } else if (value instanceof JSONArray) {
  141.                                 // 如果是数组则对数组中的元素重新排序
  142.                                 List<Object> list = new ArrayList<>();
  143.                                 JSONArray jsonArray = (JSONArray) value;
  144.                                 jsonArray.forEach(obj -> {
  145.                                         list.add((obj instanceof JSONObject) ? convertJsonObjectToMap((JSONObject) obj, supplier) : obj);
  146.                                 });
  147.                                 map.put(key, list);
  148.                         } else {
  149.                                 // 如果是普通类型则直接赋值
  150.                                 map.put(key, value);
  151.                         }
  152.                 });
  153.                 return map;
  154.         }
  155. }
复制代码
3、枚举类

(1) WxApiHeaderEnum
  1. /**
  2. * Wx API网关调用Header
  3. * @author Nick Liu
  4. * @date 2024/7/27
  5. */
  6. @Getter
  7. public enum WxApiHeaderEnum {
  8.         APP_ID("Wechatmp-Appid", "当前小程序的Appid"),
  9.         TIMESTAMP("Wechatmp-TimeStamp", "时间戳"),
  10.         SERIAL("Wechatmp-Serial", "平台证书编号,在MP管理页面获取,非证书内序列号"),
  11.         SIGNATURE("Wechatmp-Signature", "平台证书签名数据,使用base64编码"),
  12.         ;
  13.         private final String value;
  14.         private final String desc;
  15.         WxApiHeaderEnum(String value, String desc) {
  16.                 this.value = value;
  17.                 this.desc = desc;
  18.         }
  19. }
复制代码
(2) WxApiMsgTypeEnum
  1. /**
  2. * @author Nick Liu
  3. * @date 2024/7/24
  4. */
  5. @Getter
  6. public enum WxApiMsgTypeEnum {
  7.         /**
  8.          * 获取稳定版接口调用凭据
  9.          */
  10.         GET_ACCESS_TOKEN("/cgi-bin/stable_token", HttpMethod.POST, false),
  11.         /**
  12.          * 查询每日调用接口的额度,调用次数,频率限制
  13.          */
  14.         GET_API_QUOTA("/cgi-bin/openapi/quota/get", HttpMethod.POST, false),
  15.         /**
  16.          * 查询小程序域名配置信息
  17.          */
  18.         GET_DOMAIN_INFO("/wxa/getwxadevinfo", HttpMethod.POST, true),
  19.         /**
  20.          * 小程序登录
  21.          */
  22.         LOGIN("/cgi-bin/stable_token", HttpMethod.GET, false);
  23.         ;
  24.         /**
  25.          * URL路径
  26.          */
  27.         private final String urlPath;
  28.         /**
  29.          * 支持的HTTP请求方式
  30.          */
  31.         private final HttpMethod httpMethod;
  32.         /**
  33.          * 是否支持安全鉴权,可鉴权的API参考:<a href=https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc>微信Open API</a>
  34.          */
  35.         private final boolean supportSecurityAuth;
  36.         WxApiMsgTypeEnum(String urlPath, HttpMethod httpMethod, boolean supportSecurityAuth) {
  37.                 this.urlPath = urlPath;
  38.                 this.httpMethod = httpMethod;
  39.                 this.supportSecurityAuth = supportSecurityAuth;
  40.         }
  41.         public static WxApiMsgTypeEnum fromUrl(String urlPath) {
  42.                 return Arrays.stream(WxApiMsgTypeEnum.values()).filter(e -> e.urlPath.equals(urlPath)).findAny().orElse(null);
  43.         }
  44. }
复制代码
(3) BizExceptionEnum
  1. @Getter
  2. public enum BizExceptionEnum {
  3.         INVALID_PARAMS("A0101", "Invalid request params"),
  4.         SYSTEM_ERROR("B0001","System exception, please concat customer service"),
  5.         WX_GATEWAY_SYSTEM_ERROR("wx_5000", "WX gateway invocation system error"),
  6.         WX_GATEWAY_BIZ_ERROR("wx_5001", "WX gateway invocation biz error"),
  7.         ;
  8.         private final String code;
  9.         private final String message;
  10.         BizExceptionEnum(String code, String message) {
  11.                 this.code = code;
  12.                 this.message = message;
  13.         }
  14.         public static BizExceptionEnum fromCode(String code) {
  15.                 return Arrays.stream(BizExceptionEnum.values())
  16.                         .filter(bizExceptionEnum -> bizExceptionEnum.code.equals(code))
  17.                         .findAny()
  18.                         .orElse(null);
  19.         }
  20. }
复制代码
4、网关核心调用抽象类

  1. /**
  2. * 微信API网关调用封装,包括安全鉴权(加解密,加验签),数据转换等。<br/>
  3. * 安全鉴权需要在小程序管理后台开启
  4. * @author Nick Liu
  5. * @date 2024/7/24
  6. */
  7. @Slf4j
  8. public abstract class AbstractWxApiGatewayInvocationService {
  9.         private static final String VERTICAL_LINE_SEPARATOR = "|";
  10.         private static final String NEW_LINE_SEPARATOR = "\n";
  11.         @Autowired
  12.         private WxApiGatewayProperties wxApiGatewayProperties;
  13.         @Autowired
  14.         private HttpClientManager httpClientManager;
  15.         /**
  16.          * 预处理请求负载,填充安全字段
  17.          * @param payload
  18.          * @param <T>
  19.          */
  20.         private <T extends WxApiGatewayBaseDTO> void preProcess(T payload) {
  21.                 payload.setAppid(wxApiGatewayProperties.getAppId());
  22.                 payload.setNonce(WxApiCryptoUtils.generateNonce());
  23.                 payload.setTimestamp(DateTimeUtils.getUnixTimestamp());
  24.         }
  25.         /**
  26.          * 请求数据加密
  27.          * @param requestUrl 当前请求API的URL,不包括URL参数(URL Query),需要带HTTP协议头
  28.          * @param payload 请求负载
  29.          * @return 响应内容
  30.          * @param <T> 响应内容参数泛型
  31.          * @throws Exception
  32.          */
  33.         private <T extends WxApiGatewayBaseDTO> WxApiGatewayRequest encryptRequest(String requestUrl, T payload) throws Exception {
  34.                 String appId = wxApiGatewayProperties.getAppId();
  35.                 String sn = wxApiGatewayProperties.getSymmetricSn();
  36.                 String secretKey = wxApiGatewayProperties.getAesKey();
  37.                 long timeStamp = payload.getTimestamp();
  38.                 List<String> aadParamList = Arrays.asList(requestUrl, appId, String.valueOf(timeStamp), sn);
  39.                 String aad = StringUtils.join(aadParamList, VERTICAL_LINE_SEPARATOR);
  40.                 String iv = WxApiCryptoUtils.generateRandomIV();
  41.                 String plainText = FastJsonUtils.toJsonString(payload);
  42.                 return WxApiCryptoUtils.encryptByAES(secretKey, iv, aad, plainText);
  43.         }
  44.         /**
  45.          * 请求签名
  46.          * @param requestUrl 请求URL
  47.          * @param plainPayload 明文请求负载
  48.          * @param cipherPayload 密文请求负载
  49.          * @return Base64签名字符串
  50.          * @param <T> 请求参数泛型
  51.          * @throws Exception
  52.          */
  53.         private <T extends WxApiGatewayBaseDTO> String sign(String requestUrl, T plainPayload, String cipherPayload) throws Exception {
  54.                 String appId = wxApiGatewayProperties.getAppId();
  55.                 String privateKey = wxApiGatewayProperties.getPrivateKey();
  56.                 long timestamp = plainPayload.getTimestamp();
  57.                 List<String> signDataList = Arrays.asList(requestUrl, appId, String.valueOf(timestamp), cipherPayload);
  58.                 String signData = StringUtils.join(signDataList, NEW_LINE_SEPARATOR);
  59.                 return WxApiCryptoUtils.signByRSAWithSHA256(privateKey, signData);
  60.         }
  61.         /**
  62.          * 响应解密
  63.          * @param requestUrl 请求url
  64.          * @param respHeaders 响应头
  65.          * @param resp 加密响应数据
  66.          * @return 解密后的响应报文
  67.          * @throws Exception
  68.          */
  69.         private String decryptResp(String requestUrl, Map<String, String> respHeaders, WxApiGatewayResponse resp) throws Exception {
  70.                 String respTimestamp = respHeaders.get(WxApiHeaderEnum.TIMESTAMP.getValue());
  71.                 String appId = wxApiGatewayProperties.getAppId();
  72.                 String sn = wxApiGatewayProperties.getSymmetricSn();
  73.                 String secretKey = wxApiGatewayProperties.getAesKey();
  74.                 List<String> aadParamList = Arrays.asList(requestUrl, appId, respTimestamp, sn);
  75.                 String aad = StringUtils.join(aadParamList, VERTICAL_LINE_SEPARATOR);
  76.                 return WxApiCryptoUtils.decryptByAES(secretKey, aad, resp);
  77.         }
  78.         /**
  79.          * 响应验签
  80.          * @param requestUrl 请求url
  81.          * @param respHeaders 响应头
  82.          * @param resp 加密后的响应数据
  83.          * @return 是否验签通过
  84.          * @throws Exception
  85.          */
  86.         private boolean verifySignature(String requestUrl, Map<String, String> respHeaders, WxApiGatewayResponse resp)
  87.                 throws Exception {
  88.                 String appId = wxApiGatewayProperties.getAppId();
  89.                 String certificate = wxApiGatewayProperties.getCertificate();
  90.                 String respTimestamp = respHeaders.get(WxApiHeaderEnum.TIMESTAMP.getValue());
  91.                 String respDataStr = FastJsonUtils.toJsonString(resp);
  92.                 String signature = respHeaders.get(WxApiHeaderEnum.SIGNATURE.getValue());
  93.                 List<String> aadParamList = Arrays.asList(requestUrl, appId, respTimestamp, respDataStr);
  94.                 String payload = StringUtils.join(aadParamList, NEW_LINE_SEPARATOR);
  95.                 return WxApiCryptoUtils.verifySignature(payload, certificate, signature);
  96.         }
  97.         protected abstract BizException processInvocationException(Exception e);
  98.         /**
  99.          *  发送GET请求到微信API网关
  100.          * @param msgType 消息类型
  101.          * @param urlParams URL参数
  102.          * @param clazz 返回明文Class实例
  103.          * @return 明文响应内容
  104.          * @param <T> 业务请求负载泛型
  105.          * @param <U> 业务请求URL参数泛型
  106.          * @param <R> 业务返回响应泛型
  107.          * @throws Exception
  108.          */
  109.         protected <T extends WxApiGatewayBaseDTO, U extends WxApiGatewayUrlParamBaseDTO, R extends WxApiGatewayBaseVO> R sendGetToWxApiGateway(
  110.                 WxApiMsgTypeEnum msgType, U urlParams, Class<R> clazz) {
  111.                 try {
  112.                         return this.sendRequestToWxApiGateway(msgType, urlParams, null, clazz);
  113.                 } catch (Exception e) {
  114.                         log.error("微信API网关调用异常: {}", e.getMessage(), e);
  115.                         throw this.processInvocationException(e);
  116.                 }
  117.         }
  118.         /**
  119.          *  发送POST请求到微信API网关
  120.          * @param msgType 消息类型
  121.          * @param urlParams URL参数
  122.          * @param payload 请求负载: 只有POST请求才有
  123.          * @param clazz 返回明文Class实例
  124.          * @return 明文响应内容
  125.          * @param <T> 业务请求负载泛型
  126.          * @param <U> 业务请求URL参数泛型
  127.          * @param <R> 业务返回响应泛型
  128.          * @throws Exception
  129.          */
  130.         protected <T extends WxApiGatewayBaseDTO, U extends WxApiGatewayUrlParamBaseDTO, R extends WxApiGatewayBaseVO> R sendPostToWxApiGateway(
  131.                 WxApiMsgTypeEnum msgType, U urlParams, T payload, Class<R> clazz) {
  132.                 try {
  133.                         return this.sendRequestToWxApiGateway(msgType, urlParams, payload, clazz);
  134.                 } catch (Exception e) {
  135.                         log.error("微信API网关调用异常: {}", e.getMessage(), e);
  136.                         throw this.processInvocationException(e);
  137.                 }
  138.         }
  139.         /**
  140.          *  发送请求到微信API网关
  141.          * @param msgType 消息类型
  142.          * @param urlParams URL参数
  143.          * @param payload 请求负载: 只有POST请求才有
  144.          * @param clazz 返回明文Class实例
  145.          * @return 明文响应内容
  146.          * @param <T> 业务请求负载泛型
  147.          * @param <U> 业务请求URL参数泛型
  148.          * @param <R> 业务返回响应泛型
  149.          * @throws Exception
  150.          */
  151.         private <T extends WxApiGatewayBaseDTO, U extends WxApiGatewayUrlParamBaseDTO, R extends WxApiGatewayBaseVO> R sendRequestToWxApiGateway(
  152.                 WxApiMsgTypeEnum msgType, U urlParams, @Nullable T payload, Class<R> clazz) throws Exception {
  153.                 // 1、拼接完整的URL
  154.                 String host = wxApiGatewayProperties.getHost();
  155.                 String urlParamsStr = this.generateUrlParams(urlParams);
  156.                 String requestUrl = host + msgType.getUrlPath();
  157.                 String fullRequestUrl = requestUrl + urlParamsStr;
  158.                 // 2、GET请求不支持安全授权,直接发起网关调用
  159.                 if (HttpMethod.GET == msgType.getHttpMethod()) {
  160.                         log.info("微信API网关[GET]请求, url: [{}]", requestUrl);
  161.                         HttpClientResp httpClientResp = httpClientManager.get(fullRequestUrl);
  162.                         String respStr = httpClientResp.getRespContent();
  163.                         log.info("微信API网关[GET]响应, url: [{}], 响应内容:{}", requestUrl, respStr);
  164.                         R response = FastJsonUtils.toJavaBean(respStr, clazz);
  165.                         this.processRespCode(response);
  166.                         return response;
  167.                 }
  168.                 // 3、只有post请求且需要安全验证才验签
  169.                 if (HttpMethod.POST == msgType.getHttpMethod() && msgType.isSupportSecurityAuth()) {
  170.                         // 参数预处理,填充安全字段
  171.                         this.preProcess(payload);
  172.                         // 3.2 请求加密
  173.                         WxApiGatewayRequest wxApiGatewayRequest = this.encryptRequest(requestUrl, payload);
  174.                         String plainReqStr = FastJsonUtils.toJsonString(payload);
  175.                         String cipherReqStr = FastJsonUtils.toKeyLinkedJsonString(wxApiGatewayRequest);
  176.                         // 3.1 签名
  177.                         String signature = this.sign(requestUrl, payload, cipherReqStr);
  178.                         Map<String, Object> headers = new HashMap<>();
  179.                         headers.put(WxApiHeaderEnum.APP_ID.getValue(), payload.getAppid());
  180.                         headers.put(WxApiHeaderEnum.TIMESTAMP.getValue(), payload.getTimestamp());
  181.                         headers.put(WxApiHeaderEnum.SIGNATURE.getValue(), signature);
  182.                         String headersStr = FastJsonUtils.toJsonString(headers);
  183.                         // 3.3 发起网关调用
  184.                         log.info("微信API网关[POST]请求, url: [{}], 请求明文:{}", requestUrl, plainReqStr);
  185.                         log.info("微信API网关[POST]请求, url: [{}], 请求头:{}, 请求密文:{}", requestUrl, headersStr, cipherReqStr);
  186.                         HttpClientResp httpClientResp = httpClientManager.postInJson(fullRequestUrl, headers, cipherReqStr);
  187.                         String cipherRespStr = httpClientResp.getRespContent();
  188.                         // String respHeaderStr = FastJsonUtils.toJsonString(httpClientResp.getHeaders());
  189.                         log.info("微信API网关[POST]响应, url: [{}], 响应密文:{}", requestUrl, cipherRespStr);
  190.                         // 响应可能会失败,解密前处理特殊情况
  191.                         R response = FastJsonUtils.toJavaBean(cipherRespStr, clazz);
  192.                         this.processRespCode(response);
  193.                         // 3.4 解密响应报文
  194.                         WxApiGatewayResponse cipherResp = FastJsonUtils.toJavaBean(cipherRespStr, WxApiGatewayResponse.class);
  195.                         String plainRespStr = this.decryptResp(requestUrl, httpClientResp.getHeaders(), cipherResp);
  196.                         log.info("微信API网关[POST]响应, url: [{}], 响应明文:{}", requestUrl, plainRespStr);
  197.                         return FastJsonUtils.toJavaBean(plainRespStr, clazz);
  198.                 }
  199.                 // 4、只需POST请求无需验签
  200.                 if (HttpMethod.POST == msgType.getHttpMethod()) {
  201.                         String plainRequestStr = FastJsonUtils.toJsonString(payload);
  202.                         log.info("微信API网关[POST]请求, url: [{}], 请求明文:{}", requestUrl, plainRequestStr);
  203.                         HttpClientResp httpClientResp = httpClientManager.postInJson(fullRequestUrl, plainRequestStr);
  204.                         String plainRespStr = httpClientResp.getRespContent();
  205.                         log.info("微信API网关[POST]响应, url: [{}], 响应明文:{}", requestUrl, plainRespStr);
  206.                         R response = FastJsonUtils.toJavaBean(plainRespStr, clazz);
  207.                         this.processRespCode(response);
  208.                         return response;
  209.                 }
  210.                 throw new UnsupportedOperationException("只支持GET或者POST请求");
  211.         }
  212.         private <R extends WxApiGatewayBaseVO> void processRespCode(R response) {
  213.                 if (!Objects.isNull(response.getErrorCode()) && WxApiGatewayErrorCode.SUCCESS != response.getErrorCode()) {
  214.                         throw new BizException(BizExceptionEnum.WX_GATEWAY_BIZ_ERROR, response.getErrorMsg());
  215.                 }
  216.         }
  217.         /**
  218.          * 生成URL参数
  219.          * @param urlParam URL参数实例
  220.          * @return 带?的参数字符串
  221.          * @param <U> URL参数泛型
  222.          * @throws Exception
  223.          */
  224.         private <U extends WxApiGatewayUrlParamBaseDTO> String generateUrlParams(U urlParam) throws Exception {
  225.                 if (Objects.isNull(urlParam)) {
  226.                         return StringUtils.EMPTY;
  227.                 }
  228.                 Field[] fields = urlParam.getClass().getDeclaredFields();
  229.                 if (ArrayUtils.isEmpty(fields)) {
  230.                         return StringUtils.EMPTY;
  231.                 }
  232.                 StringBuilder urlPramsBuilder = new StringBuilder("?");
  233.                 for (Field field : fields) {
  234.                         field.setAccessible(true);
  235.                         JSONField jsonField = field.getAnnotation(JSONField.class);
  236.                         String fieldName = Objects.isNull(jsonField) ? field.getName() : jsonField.name();
  237.                         Object fieldValue = field.get(urlParam);
  238.                         if (!Objects.isNull(fieldValue)) {
  239.                                 urlPramsBuilder.append(fieldName).append("=").append(fieldValue).append("&");
  240.                         }
  241.                 }
  242.                 urlPramsBuilder.deleteCharAt(urlPramsBuilder.length() - 1);
  243.                 return urlPramsBuilder.toString();
  244.         }
  245. }
复制代码
5、网关核心调用业务类

  1. /**
  2. * 微信API网关调用器,指定消息类型,业务请求参数和响应内容类型即可
  3. * @author Nick Liu
  4. * @date 2024/7/27
  5. */
  6. @Slf4j
  7. @Service
  8. public class WxApiGatewayInvoker extends AbstractWxApiGatewayInvocationService {
  9.         @Override
  10.         protected BizException processInvocationException(Exception e) {
  11.                 if (e instanceof BizException) {
  12.                         throw (BizException) e;
  13.                 }
  14.                 return new BizException(BizExceptionEnum.WX_GATEWAY_SYSTEM_ERROR);
  15.         }
  16.         /**
  17.          * 获取稳定版本接口调用凭证
  18.          * @param stableAccessTokenDTO 获取稳定版本Token业务参数
  19.          * @return
  20.          */
  21.         public StableAccessTokenVO getStableAccessToken(StableAccessTokenDTO stableAccessTokenDTO) {
  22.                 return super.sendPostToWxApiGateway(WxApiMsgTypeEnum.GET_ACCESS_TOKEN, null, stableAccessTokenDTO, StableAccessTokenVO.class);
  23.         }
  24.         /**
  25.          * 查询API调用额度
  26.          * @param genericUrlParamsDTO
  27.          * @param apiQuotaDTO
  28.          * @return
  29.          */
  30.         public ApiQuotaVO getApiQuota(GenericUrlParamsDTO genericUrlParamsDTO, ApiQuotaDTO apiQuotaDTO) {
  31.                 return super.sendPostToWxApiGateway(WxApiMsgTypeEnum.GET_API_QUOTA, genericUrlParamsDTO, apiQuotaDTO, ApiQuotaVO.class);
  32.         }
  33.         /**
  34.          * 查询域名配置
  35.          * @param genericUrlParamsDTO
  36.          * @param domainInfoDTO
  37.          * @return
  38.          */
  39.         public DomainInfoVO getDomainInfo(GenericUrlParamsDTO genericUrlParamsDTO, DomainInfoDTO domainInfoDTO){
  40.                 return super.sendPostToWxApiGateway(WxApiMsgTypeEnum.GET_DOMAIN_INFO, genericUrlParamsDTO, domainInfoDTO, DomainInfoVO.class);
  41.         }
  42.         /**
  43.          * 小程序登录接口
  44.          * @param miniProgramLoginDTO 小程序登录接口业务参数
  45.          * @return
  46.          */
  47.         public MiniProgramLoginVO login(MiniProgramLoginDTO miniProgramLoginDTO) {
  48.                 return super.sendGetToWxApiGateway(WxApiMsgTypeEnum.GET_ACCESS_TOKEN, miniProgramLoginDTO, MiniProgramLoginVO.class);
  49.         }
  50. }
复制代码

六、测试用例

1、application.yml

  1. # http client configuration
  2. http:
  3.   client:
  4.     max-total: 500 # 连接池最大连接数
  5.     default-max-per-route: 100 # 每个路由最大连接数
  6.     max-idle-time: 5s # 连接最大空闲时间
  7.     connection-request-timeout: 3s # 从连接池获取连接超时时间
  8.     connection-timeout: 5s # 与服务端建立连接超时时间
  9.     socket-timeout: 10s # 客户端从服务器读取数据超时时间
  10. # 微信API网关配置
  11. wx:
  12.   gateway:
  13.     host: https://api.weixin.qq.com
  14.     app-id: appId
  15.     app-secret: appSecret
  16.     # 对称密钥证书编号
  17.     symmetric-sn: xxx
  18.     # 非对称密钥证书编号
  19.     asymmetric-sn: xxx
  20.     # AES秘钥
  21.     aes-key: xxxxxxxxxxxxxxxxxxxxxx
  22.     # 加签私钥
  23.     private-key: xxxxxxxxxxxxxxxxxxxxxx
  24.     # 验签证书
  25.     certificate: xxxxxxxxxxxxxxxxxxxxxx
复制代码
2、相干业务类

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

(1) StableAccessTokenDTO
  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class StableAccessTokenDTO extends WxApiGatewayBaseDTO {
  6.         /**
  7.          * 填写固定值 client_credential
  8.          */
  9.         @JSONField(name = "grant_type")
  10.         private String grantType = "client_credential";
  11.         /**
  12.          * 账号唯一凭证,即 AppID
  13.          */
  14.         @JSONField(name = "appid")
  15.         private String appId;
  16.         /**
  17.          * 账号唯一凭证密钥,即 AppSecret
  18.          */
  19.         private String secret;
  20.         /**
  21.          * 默认使用 false。
  22.          * 1. force_refresh = false 时为普通调用模式,access_token 有效期内重复调用该接口不会更新 access_token;
  23.          * 2. 当force_refresh = true 时为强制刷新模式,会导致上次获取的 access_token 失效,并返回新的 access_token
  24.          */
  25.         @JSONField(name = "force_refresh")
  26.         private boolean forceRefresh;
  27. }
复制代码
(2) StableAccessTokenVO
  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class StableAccessTokenVO extends WxApiGatewayBaseVO {
  6.         /**
  7.          * 获取到的凭证
  8.          */
  9.         @JSONField(name = "access_token")
  10.         private String accessToken;
  11.         /**
  12.          * 凭证有效时间,单位:秒。目前是7200秒之内的值。
  13.          */
  14.         @JSONField(name = "expires_in")
  15.         private Integer expiresIn;
  16. }
复制代码
2) 查询小程序域名设置信息

(1) DomainInfoDTO
  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class DomainInfoDTO extends WxApiGatewayBaseDTO {
  6.         /**
  7.          * 查询配置域名的类型, 可选值如下:
  8.          * 1. getbizdomain 返回业务域名
  9.          * 2. getserverdomain 返回服务器域名
  10.          * 3. 不指明返回全部
  11.          */
  12.         private String action;
  13. }
复制代码
(2) DomainInfoVO
  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class DomainInfoVO extends WxApiGatewayBaseVO {
  6.         @JSONField(name = "requestdomain")
  7.         private List<String> requestDomain;
  8. }
复制代码
3、WxApiGatewayController

  1. @RestController
  2. @RequestMapping("/wx/api")
  3. @RequiredArgsConstructor
  4. public class WxApiGatewayController {
  5.         private final WxApiGatewayInvoker wxApiGatewayInvoker;
  6.         @PostMapping("/access-token/stable")
  7.         public ApiResponse<StableAccessTokenVO> getStableAccessToken(@RequestBody StableAccessTokenDTO stableAccessTokenDTO) {
  8.                 return ApiResponse.success(wxApiGatewayInvoker.getStableAccessToken(stableAccessTokenDTO));
  9.         }
  10.         @PostMapping("/domain/info")
  11.         public ApiResponse<DomainInfoVO> getApiQuota(@RequestParam String accessToken, @RequestBody DomainInfoDTO domainInfoDTO) {
  12.                 GenericUrlParamsDTO genericUrlParamsDTO = GenericUrlParamsDTO.builder().accessToken(accessToken).build();
  13.                 return ApiResponse.success(wxApiGatewayInvoker.getDomainInfo(genericUrlParamsDTO, domainInfoDTO));
  14.         }
  15. }
复制代码
4、测试结果

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

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

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

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



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

张国伟

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表