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

目录

  • 一、序言
  • 二、前置准备
    • 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可参考支持的接口调用。

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


二、前置准备

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

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;}@Overridepublic 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";@Autowiredprivate WxApiGatewayProperties wxApiGatewayProperties;@Autowiredprivate 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、拼接完整的URLString 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 {@Overrideprotected 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.comapp-id: appIdapp-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) 查询小程序域名配置信息测试

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

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/437473.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

毕业论文设计javaweb+VUE高校教师信息管理系统

目录 一、系统概述 二、功能详解 1. 教师管理 2. 部门管理 3. 奖惩管理 4. 业绩管理 5. 培训管理 6. 报表查询 三、总结 四、示例代码 1 前端VUE 2 后端SpringBootjava 3 数据库表 随着教育信息化的发展&#xff0c;传统的手工管理方式已经不能满足现代学校对教师…

自动驾驶系列—自动驾驶发展史介绍

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

PyCharm开发工具的安装和基础使用

打开官网&#xff1a;https://www.jetbrains.com/ 切换中文语言&#xff0c; 点击开发者工具 → 选择PyCharm&#xff0c; 点击下载&#xff0c; 初学者下载免费使用的社区版&#xff08;community&#xff09;就够了&#xff0c; 点击下载&#xff0c; 点击下一步&am…

高性能架构—存储高性能

1 &#x1f4ca;关系型数据库 存储技术飞速发展&#xff0c;关系型数据的ACID特性以及强大的SQL查询让其成为各种业务系统的关键和核心存储系统。 很多场景下的高性能设计最核心的就是关系型数据库的设计&#xff0c;很多数据库厂商再优化和提升单个数据库服务器的性能方面做了…

Java Web应用升级故障案例解析

在一次Java Web应用程序的优化升级过程中&#xff0c;从Tomcat 7.0.109版本升级至8.5.93版本后&#xff0c;尽管在预发布环境中验证无误&#xff0c;但在灰度环境中却发现了一个令人困惑的问题&#xff1a;新日志记录神秘“失踪”。本文深入探讨了这一问题的排查与解决过程&…

激光切割机适用材质有哪些

激光切割机是一种利用激光束对各种材料进行高精度、高速度切割的机器设备。其适用材质广泛&#xff0c;包括但不限于以下两大类&#xff1a; 一、金属材料 不锈钢&#xff1a;激光切割机较容易切割不锈钢薄板&#xff0c;使用高功率YAG激光切割系统&#xff0c;切割不锈钢板的…

大厂面试真题-说一下Mybatis的缓存

首先看一下原理图 Mybatis提供了两种缓存机制&#xff1a;一级缓存&#xff08;L1 Cache&#xff09;和二级缓存&#xff08;L2 Cache&#xff09;&#xff0c;旨在提高数据库查询的性能&#xff0c;减少数据库的访问次数。注意查询的顺序是先二级缓存&#xff0c;再一级缓存。…

死锁的成因与解决方案

目录 死锁的概念与成因 栗子 死锁的情况 哲学家问题 如何避免死锁 必要条件 死锁的解决方案 总结 死锁的概念与成因 多个线程同时被阻塞,他们中的其中一个或者全部都在等待某个资源的释放,导致线程无限期被阻塞,程序无法停止 栗子 我和美女a出去吃饺子,吃饺子要醋和酱油…

高中教辅汇总【35GB】

文章目录 一、资源概览二、资源亮点三、获取方式 一、资源概览 这份教辅资源汇总&#xff0c;精心搜集了高中各学科的海量教辅资料&#xff0c;总容量高达35GB&#xff0c;覆盖了语文、数学、英语、物理、化学、生物、历史、地理、政治等所有必修及选修科目。从基础知识点到难…

ros2 自定义工作空间添加source

新建一个工作空间&#xff1a;ros2 create pkg~~~~~~~~~~~~ colcon build之后 &#xff0c;在install文件夹里面有一个 setup,bash文件 将这个文件添加到 bashrc gedit .bashrc 这样 在一个新终端中可以直接运行ros2 run package name &#xff08;包名&#xff09; 可执行…

针对考研的C语言学习(2019链表大题)

题目解析&#xff1a; 【考】双指针算法&#xff0c;逆置法&#xff0c;归并法。 解析&#xff1a;因为题目要求空间复杂度为O(1)&#xff0c;即不能再开辟一条链表&#xff0c;因此我们只能用变量来整体挪动原链表。 第一步先找出中间节点 typedef NODE* Node; Node find_m…

鸿蒙NEXT开发-自定义构建函数(基于最新api12稳定版)

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…

基于Word2Vec和LSTM实现微博评论情感分析

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色…

css动态边框

参考&#xff1a; Clip-path实现按钮流动边框动画_在线clip-path-CSDN博客 https://www.5axxw.com/questions/simple/9ju5yt#google_vignette <div class"bottom-top-item-centent bottom-top-item-left"><vue-seamless-scroll :data"listLeftData&q…

【算法】链表:206.反转链表(easy)

系列专栏 《分治》 《模拟》 《Linux》 目录 1、题目链接 2、题目介绍 3、解法&#xff08;快慢指针&#xff09; 解题步骤&#xff1a; 关键点&#xff1a; 复杂度分析&#xff1a; 4、代码 1、题目链接 206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; …

如何用JavaScript编写一个简单的计数器

在网页开发中&#xff0c;计数器是一种常见的功能&#xff0c;它可以帮助我们记录点击次数、显示时间等。下面我将介绍如何在HTML页面中使用JavaScript实现一个基本的计数器。如图&#xff1a; 1、 创建HTML结构 首先&#xff0c;我们需要创建一个基础的HTML结构来容纳我们的计…

自动驾驶系列—深度剖析自动驾驶芯片SoC架构:选型指南与应用实战

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

物联网将如何影响全球商业?

互联网使人们能够交流&#xff0c;企业能够全天候不间断地跨洋跨洲持续运营。它重塑、颠覆并催生了新的产业&#xff0c;改变了人类与世界互动的方式。互联网曾经仅仅是一种方便、快捷、廉价的向世界各地发送信息的方式&#xff0c;而现在&#xff0c;只需打开或关闭任何连接到…

【C++】树形结构的关联式容器:set、map、multiset、multimap的使用

&#x1f33b;个人主页&#xff1a;路飞雪吖~ ✨专栏&#xff1a;C/C 目录 一、set的简单介绍和使用 &#x1f31f;set的介绍 &#x1f525;注意&#xff1a; &#x1f320;小贴士&#xff1a; &#x1f31f;set的使用 ✨set的构造 ✨set的迭代器 ​编辑 ✨set的容量 …

Html--笔记01:使用软件vscode,简介Html5--基础骨架以及标题、段落、图片标签的使用

一.使用VSC--全称&#xff1a;Visual Studio Code vscode用来写html文件&#xff0c;打开文件夹与创建文件夹&#xff1a;①选择文件夹 ②拖拽文件 生成浏览器的html文件的快捷方式&#xff1a; &#xff01;enter 运行代码到网页的方法&#xff1a; 普通方法&#xff1a…