• JAVA-WEB项目中,前后台结合AES和RSA对数据加密处理


    实际项目中为了系统安全,我们经常需要对请求数据和响应数据做加密处理,这里以spring后台,vue前台的java web为例,记录一个实现过程

    一、为什么要结合AES和RSA?

    因为AES是对称加密,即加密解密用的秘钥是一样,这样一来AES的秘钥保管尤其重要,但是AES有个很好的优点,就是处理效率高。而RSA是不对称加密,即加密解密用的秘钥不一样,分别叫公钥和私钥,通常用公钥加密,然后用私钥解密,其中公钥可以公开,只需要保管好私钥即可,而相比AES而言RSA速度慢效率低。所以,通常我们结合这两种加密方式的优点来完成数据的安全传输。

    二、AES和RSA的结合使用过程

    1.前端随机动态生成aesKey:因为AES的加密解密秘钥需要一致,如果整个系统写死AES的秘钥会很不安全,所以每次请求动态生成aesKey会比较好

    2.前端用RSA对动态aesKey加密:动态aesKey需要传到后端供解密,传输过程用RSA加密

    3.前端保存动态aesKey:因为同一个请求的响应需要一样的aesKey解密,所以前端还得把动态aesKey保存下来,可以再随机生成一个id,然后按键值对的方式保存在前端变量中{id: aesKey}

    4.把加密的aesKey和id放到请求头

    5.后端用RSA私钥解密得明文aesKey:后端从请求头取出加密的aesKey,然后用私钥解密拿到明文的aesKey,然后对请求数据解密

    6.后端用明文aesKey加密响应数据

    7.后端把请求过来的id放到响应头

    8.前端根据响应头的id,取到对应的aesKey,对响应数据解密

    9.前端删除动态的aesKey

    三、具体实现案例

    1.前端实现AES和RSA的公共方法

            aesUtils.js:

    1. const CryptoJS = require('crypto-js')
    2. function GetRandomNum (n) {
    3. let chars = [
    4. '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F',
    5. 'G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V',
    6. 'W','X','Y','Z','.','?','~','!','@','#','$','%','^','&','*']
    7. if(n == null) {
    8. n = 16
    9. }
    10. let res = ""
    11. for(let i = 0; i < n ; i++) {
    12. let id = Math.ceil(Math.random()*46)
    13. res += chars[id]
    14. }
    15. return res
    16. }
    17. function GetUuid () {
    18. let s = []
    19. const hexDigits = '0123456789abcdef'
    20. for (let i = 0; i < 36; i++) {
    21. let indexStart = Math.floor(Math.random() * 0x10)
    22. s[i] = hexDigits.substring(indexStart, indexStart+1)
    23. }
    24. s[14] = '4'
    25. let indexStart = (s[19] & 0x3) | 0x8
    26. s[19] = hexDigits.substring(indexStart, indexStart+1)
    27. s[8] = s[13] = s[18] = s[23] = '-'
    28. return s.join('')
    29. }
    30. function Decrypt (word, key, iv) {
    31. let key = CryptoJS.enc.Utf8.parse(key)
    32. let base64 = CryptoJS.enc.Base64.parse(word)
    33. let src = CryptoJS.enc.Base64.stringify(base64)
    34. var decrypt = CryptoJS.AES.decrypt(src, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding })
    35. var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
    36. return decryptedStr.toString()
    37. }
    38. function Encrypt (word, key, iv) {
    39. let key = CryptoJS.enc.Utf8.parse(key)
    40. let src = CryptoJS.enc.Utf8.parse(word)
    41. var encrypted = CryptoJS.AES.encrypt(src, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding })
    42. return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
    43. }
    44. export default {
    45. Decrypt,
    46. Encrypt,
    47. GetRandomNum,
    48. GetUuid,
    49. }

            rsaUtils.js:

    1. import JSEncrypt from 'jsencrypt'
    2. const pubKey = `MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMYWwlqtkWIdA0I/54TP/k1VLgyNwzQB1IvrVKdNfobivHzN02VFGAED1hDSLDiSp4yYrFcXmMFReJJOJ1zjvWECAwEAAQ==`
    3. const encrypt = new JSEncrypt()
    4. encrypt.setPublicKey(pubKey)
    5. function Encrypt(str) {
    6. let data = encrypt.encrypt(str.toString())
    7. return data
    8. }
    9. function Decrypt(str) {
    10. let data = encrypt.decrypt(str.toString())
    11. return data
    12. }
    13. export default {
    14. Encrypt,
    15. Decrypt,
    16. }

     2.前端在拦截请求处,生成AES随机秘钥并加密请求数据,再用RSA加密AES秘钥并放到请求头
            fetch.js:

    1. const aesKeys = {}
    2. instance.interceptors.request.use(function(request) {
    3. let randomKey = aes.GetRandomNum()
    4. let reqData
    5. if (request.data instanceof Object) {
    6. reqData = JSON.stringify(request.data)
    7. } else {
    8. reqData = request.data
    9. }
    10. request.data = aes.Encrypt(reqData, randomKey, randomKey)
    11. let uuid = aes.GetUuid()
    12. let encryptAesKey = rsa.Encrypt(randomKey)
    13. request.headers['EncryptAesKey'] = encryptAesKey
    14. request.headers['uuid'] = uuid
    15. aesKeys[uuid] = randomKey
    16. return request
    17. }, function(err) {
    18. Message({
    19. message: err,
    20. type: 'error'
    21. })
    22. return Promise.reject(err)
    23. })
    24. instance.interceptors.response.use(function(response) {
    25. try {
    26. let uuid = response.headers['uuid']
    27. let aesKey = aesKeys[uuid]
    28. delete aesKeys[uuid]
    29. return JSON.parse(aes.Decrypt(response.data, aesKey, aesKey))
    30. } catch (e) {
    31. return response.data
    32. }
    33. }, function(err) {
    34. Message({
    35. message: err,
    36. type: 'error'
    37. })
    38. return Promise.reject(err)
    39. })

    3.后端实现AES和RSA的公共方法
            AesUtils.java:

    1. import javax.crypto.Cipher;
    2. import javax.crypto.spec.IvParameterSpec;
    3. import javax.crypto.spec.SecretKeySpec;
    4. import org.apache.commons.codec.binary.Base64;
    5. public class AesUtil {
    6. public static String encrypt(String data, String key, String iv) throws Exception {
    7. try {
    8. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    9. int blockSize = cipher.getBlockSize();
    10. byte[] dataBytes = data.getBytes();
    11. int plaintextLength = dataBytes.length;
    12. if (plaintextLength % blockSize != 0) {
    13. plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
    14. }
    15. byte[] plaintext = new byte[plaintextLength];
    16. System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
    17. SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
    18. IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
    19. cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
    20. byte[] encrypted = cipher.doFinal(plaintext);
    21. return new Base64().encodeToString(encrypted);
    22. } catch (Exception e) {
    23. e.printStackTrace();
    24. return null;
    25. }
    26. }
    27. public static String desEncrypt(String data, String key, String iv) throws Exception {
    28. try {
    29. byte[] encrypted1 = new Base64().decode(data);
    30. Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    31. SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
    32. IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
    33. cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
    34. byte[] original = cipher.doFinal(encrypted1);
    35. String originalString = new String(original);
    36. return originalString.trim();
    37. } catch (Exception e) {
    38. e.printStackTrace();
    39. return null;
    40. }
    41. }
    42. public static String encryptWithKey(String data, String key) throws Exception {
    43. return encrypt(data, key, key);
    44. }
    45. public static String desEncryptWithKey(String data, String key) throws Exception {
    46. return desEncrypt(data, key, key);
    47. }
    48. }

            RsaUtils.java:

    1. import java.security.KeyFactory;
    2. import java.security.KeyPair;
    3. import java.security.KeyPairGenerator;
    4. import java.security.NoSuchAlgorithmException;
    5. import java.security.PrivateKey;
    6. import java.security.PublicKey;
    7. import java.security.SecureRandom;
    8. import java.security.spec.InvalidKeySpecException;
    9. import java.security.spec.PKCS8EncodedKeySpec;
    10. import java.security.interfaces.RSAPrivateKey;
    11. import java.security.interfaces.RSAPublicKey;
    12. import java.security.spec.X509EncodedKeySpec;
    13. import java.util.Base64;
    14. import java.util.HashMap;
    15. import java.util.Map;
    16. import java.util.UUID;
    17. import org.bouncycastle.asn1.ASN1Encodable;
    18. import org.bouncycastle.asn1.ASN1Primitive;
    19. import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
    20. import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
    21. import org.springframework.util.Base64Utils;
    22. import javax.crypto.Cipher;
    23. public class RSAUtils {
    24. /** 算法名称 */
    25. private static final String ALGORITHM = "RSA";
    26. /** 默认密钥大小 */
    27. private static final int KEY_SIZE = 4096;
    28. /** 密钥对生成器 */
    29. private static KeyPairGenerator keyPairGenerator = null;
    30. private static KeyFactory keyFactory = null;
    31. /** 缓存的密钥对 */
    32. private static KeyPair keyPair = null;
    33. /** Base64 编码/解码器 JDK1.8 */
    34. private static Base64.Decoder decoder = Base64.getDecoder();
    35. private static Base64.Encoder encoder = Base64.getEncoder();
    36. private static final String PRI_KEY = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAxhbCWq2RYh0DQj/nhM/+TVUuDI3DNAHUi+tUp01+huK8fM3TZUUYAQPWENIsOJKnjJisVxeYwVF4kk4nXOO9YQIDAQABAkEArOrHJBLpm0UKSDWyq2xJaEZYGVtSsD58xNtcHWN3dNRFWLZWZ9+D31OT0yE0T+dUhBVQFzHh3uDPd3Ax4STIwQIhAOPcBuFP4hoLcCPGvnvl+Co79XRKVkFtlduimiMzxg65AiEA3o2CjUz6TN51P8Q/kkPLHZHj4kB3ZPjNLNdKQusfj+kCIDe6z9/5psZR99p4OIybIYhK4+zOZaxY/ica7PIhLpbZAiEAupudZC2vktTVK2q6g0IlBd5WXlf/xMJ6B6ddtU7BYEECIEzbfR1Ac5zTrxTQ5icmD/ZRChgfhdxToGa21SQscW+K";
    37. public static final String PEM_CODE_PUB = "MEEwDQYJKoZIhvcNAQEBBQADMAAwLQImDr5/bK6tmdEMYTJXsD/AXIOwE2a9/bfkPvtWUR7vzkvB33tPcEsCAwEAAQ==";
    38. public static final String PEM_CODE_PRI = "";
    39. /** 初始化密钥工厂 */
    40. static {
    41. try {
    42. keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
    43. keyFactory = KeyFactory.getInstance(ALGORITHM);
    44. } catch (NoSuchAlgorithmException e) {
    45. e.printStackTrace();
    46. }
    47. }
    48. private RSAUtils() {
    49. }
    50. public static synchronized Map generateKeyPair() {
    51. try {
    52. keyPairGenerator.initialize(KEY_SIZE,
    53. new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes()));
    54. keyPair = keyPairGenerator.generateKeyPair();
    55. } catch (Exception e) {
    56. e.printStackTrace();
    57. }
    58. RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
    59. RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
    60. String publicKeyString = encoder.encodeToString(rsaPublicKey.getEncoded());
    61. String privateKeyString = encoder.encodeToString(rsaPrivateKey.getEncoded());
    62. Map keyPairMap = new HashMap();
    63. keyPairMap.put("public", publicKeyString);
    64. keyPairMap.put("private", privateKeyString);
    65. return keyPairMap;
    66. }
    67. public static synchronized Map generatePKS1KeyPair() {
    68. Map keyPairMap = new HashMap();
    69. try {
    70. keyPairGenerator.initialize(KEY_SIZE,
    71. new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes()));
    72. keyPair = keyPairGenerator.generateKeyPair();
    73. PublicKey pub = keyPair.getPublic();
    74. byte[] pubBytes = pub.getEncoded();
    75. SubjectPublicKeyInfo spkInfo = SubjectPublicKeyInfo.getInstance(pubBytes);
    76. ASN1Primitive pubprimitive = spkInfo.parsePublicKey();
    77. byte[] publicKeyPKCS1 = pubprimitive.getEncoded();
    78. System.out.println(publicKeyPKCS1.toString());
    79. PrivateKey priv = keyPair.getPrivate();
    80. byte[] privBytes = priv.getEncoded();
    81. PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(privBytes);
    82. ASN1Encodable encodable = pkInfo.parsePrivateKey();
    83. ASN1Primitive priprimitive = encodable.toASN1Primitive();
    84. byte[] privateKeyPKCS1 = priprimitive.getEncoded();
    85. System.out.println(privateKeyPKCS1.toString());
    86. keyPairMap.put("public", publicKeyPKCS1);
    87. keyPairMap.put("private", privateKeyPKCS1.toString());
    88. } catch (Exception e) {
    89. e.printStackTrace();
    90. }
    91. return keyPairMap;
    92. }
    93. public static PublicKey getPublicKey(String pubKey) {
    94. try {
    95. byte[] keyBytes = decoder.decode(pubKey);
    96. X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes);
    97. return keyFactory.generatePublic(x509EncodedKeySpec);
    98. } catch (InvalidKeySpecException e) {
    99. e.printStackTrace();
    100. }
    101. return null;
    102. }
    103. public static PrivateKey getPrivateKey(String priKey) {
    104. try {
    105. byte[] keyBytes = decoder.decode(priKey);
    106. PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes);
    107. return (java.security.interfaces.RSAPrivateKey) keyFactory
    108. .generatePrivate(pkcs8EncodedKeySpec);
    109. } catch (InvalidKeySpecException e) {
    110. e.printStackTrace();
    111. }
    112. return null;
    113. }
    114. public static String encryptByPublic(byte[] content, PublicKey publicKey) {
    115. if (publicKey == null) {
    116. publicKey = (PublicKey) getPublicKey(PEM_CODE_PUB);
    117. }
    118. try {
    119. Cipher cipher = Cipher.getInstance("RSA");
    120. cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    121. //该密钥能够加密的最大字节长度
    122. int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11;
    123. byte[][] arrays = splitBytes(content, splitLength);
    124. StringBuffer stringBuffer = new StringBuffer();
    125. for (byte[] array : arrays) {
    126. stringBuffer.append(Base64Utils.encodeToString(cipher.doFinal(array)));
    127. }
    128. return stringBuffer.toString();
    129. } catch (Exception e) {
    130. e.printStackTrace();
    131. }
    132. return null;
    133. }
    134. public static String encryptByPrivate(byte[] content, PrivateKey privateKey) {
    135. if (privateKey == null) {
    136. privateKey = (PrivateKey) getPrivateKey(PEM_CODE_PRI);
    137. }
    138. try {
    139. Cipher cipher = Cipher.getInstance("RSA");
    140. cipher.init(Cipher.ENCRYPT_MODE, privateKey);
    141. //该密钥能够加密的最大字节长度
    142. int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11;
    143. byte[][] arrays = splitBytes(content, splitLength);
    144. StringBuffer stringBuffer = new StringBuffer();
    145. for (byte[] array : arrays) {
    146. stringBuffer.append(Base64Utils.encodeToString(cipher.doFinal(array)));
    147. }
    148. return stringBuffer.toString();
    149. } catch (Exception e) {
    150. e.printStackTrace();
    151. }
    152. return null;
    153. }
    154. public static String decryptByPrivate(String content, PrivateKey privateKey) {
    155. if (privateKey == null) {
    156. privateKey = (PrivateKey) getPrivateKey(PEM_CODE_PRI);
    157. }
    158. try {
    159. Cipher cipher = Cipher.getInstance("RSA");
    160. cipher.init(Cipher.DECRYPT_MODE, privateKey);
    161. //该密钥能够加密的最大字节长度
    162. int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8;
    163. byte[] contentBytes = Base64Utils.decodeFromString(content);
    164. byte[][] arrays = splitBytes(contentBytes, splitLength);
    165. StringBuffer stringBuffer = new StringBuffer();
    166. for (byte[] array : arrays) {
    167. stringBuffer.append(new String(cipher.doFinal(array)));
    168. }
    169. return stringBuffer.toString();
    170. } catch (Exception e) {
    171. e.printStackTrace();
    172. }
    173. return null;
    174. }
    175. public static byte[][] splitBytes(byte[] bytes, int splitLength) {
    176. //bytes与splitLength的余数
    177. int remainder = bytes.length % splitLength;
    178. //数据拆分后的组数,余数不为0时加1
    179. int quotient = remainder != 0 ? bytes.length / splitLength + 1
    180. : bytes.length / splitLength;
    181. byte[][] arrays = new byte[quotient][];
    182. byte[] array = null;
    183. for (int i = 0; i < quotient; i++) {
    184. //如果是最后一组(quotient-1),同时余数不等于0,就将最后一组设置为remainder的长度
    185. if (i == quotient - 1 && remainder != 0) {
    186. array = new byte[remainder];
    187. System.arraycopy(bytes, i * splitLength, array, 0, remainder);
    188. } else {
    189. array = new byte[splitLength];
    190. System.arraycopy(bytes, i * splitLength, array, 0, splitLength);
    191. }
    192. arrays[i] = array;
    193. }
    194. return arrays;
    195. }
    196. }

    4.后端重写HttpServletRequestWrapper中的getInputStream(),解密拿到请求的明文

    1. import org.apache.commons.lang3.StringUtils;
    2. import javax.servlet.ReadListener;
    3. import javax.servlet.ServletInputStream;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletRequestWrapper;
    6. import java.io.BufferedReader;
    7. import java.io.ByteArrayInputStream;
    8. import java.io.IOException;
    9. import java.io.InputStreamReader;
    10. import java.nio.charset.StandardCharsets;
    11. import java.util.*;
    12. public class XssRequestWrappers extends HttpServletRequestWrapper {
    13. public XssRequestWrappers(HttpServletRequest request) {
    14. super(request);
    15. }
    16. @Override
    17. public ServletInputStream getInputStream() throws IOException {
    18. Map headerMap = getHeadersInfo((HttpServletRequest) super.getRequest());
    19. String encryptAeskey = headerMap.get("encryptaeskey");
    20. String aesKey = RSAUtils.decryptByPrivate(encryptAeskey, RSAUtils.getPrivateKey(CommConstants.RSA.PRI_KEY));
    21. ServletInputStream servletInputStream = super.getInputStream();
    22. try {
    23. return decryptReqData(servletInputStream, aesKey);
    24. } catch (Exception e) {
    25. e.printStackTrace();
    26. }
    27. return servletInputStream;
    28. }
    29. private ServletInputStream decryptReqData(ServletInputStream servletInputStream, String aesKey)
    30. throws Exception {
    31. StringBuilder jb = new StringBuilder();
    32. String line;
    33. BufferedReader reader = new BufferedReader(new InputStreamReader(servletInputStream));
    34. while ((line = reader.readLine()) != null) {
    35. jb.append(line);
    36. }
    37. String encryptReqData = jb.toString();
    38. if(StringUtils.isNotBlank(encryptReqData)) {
    39. String reqData = AesUtil.desEncryptWithKey(encryptReqData, aesKey);
    40. ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(reqData.getBytes(StandardCharsets.UTF_8));
    41. return new ServletInputStream() {
    42. @Override
    43. public int read() {
    44. return byteArrayInputStream.read();
    45. }
    46. @Override
    47. public boolean isFinished() {
    48. return false;
    49. }
    50. @Override
    51. public boolean isReady() {
    52. return false;
    53. }
    54. @Override
    55. public void setReadListener(ReadListener listener) {
    56. }
    57. };
    58. }
    59. return servletInputStream;
    60. }
    61. private Map getHeadersInfo(HttpServletRequest request) {
    62. Map map = new HashMap<>();
    63. Enumeration headerNames = request.getHeaderNames();
    64. Locale.setDefault(Locale.ENGLISH);
    65. while (headerNames.hasMoreElements()) {
    66. String key = headerNames.nextElement().toLowerCase(Locale.ENGLISH);
    67. String value = request.getHeader(key);
    68. map.put(key, value);
    69. }
    70. return map;
    71. }
    72. }

    5.通过ResponseBodyAdvice给响应数据加密处理

    1. import org.apache.commons.lang3.StringUtils;
    2. import org.springframework.core.MethodParameter;
    3. import org.springframework.http.MediaType;
    4. import org.springframework.http.converter.HttpMessageConverter;
    5. import org.springframework.http.server.ServerHttpRequest;
    6. import org.springframework.http.server.ServerHttpResponse;
    7. import org.springframework.http.server.ServletServerHttpRequest;
    8. import org.springframework.web.bind.annotation.ControllerAdvice;
    9. import javax.servlet.http.HttpServletRequest;
    10. import java.util.*;
    11. @ControllerAdvice
    12. public class ResponseBodyAdvice implements org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice {
    13. @Override
    14. public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    15. try {
    16. ServletServerHttpRequest request = (ServletServerHttpRequest)serverHttpRequest;
    17. HttpServletRequest servletRequest = request.getServletRequest();
    18. Map headerNames = RequestUtils.getHeadersInfo(servletRequest);
    19. String encryptAeskey = headerNames.get("encryptaeskey");
    20. String aesKey = RSAUtils.decryptByPrivate(encryptAeskey, RSAUtils.getPrivateKey(CommConstants.RSA.PRI_KEY));
    21. String uuid = headerNames.get("uuid");
    22. serverHttpResponse.getHeaders().add("uuid", uuid);
    23. ArrayList list = new ArrayList<>();
    24. list.add("uuid");
    25. serverHttpResponse.getHeaders().setAccessControlExposeHeaders(list);
    26. return AesUtil.encryptWithKey(String.valueOf(o), aesKey);
    27. } catch (Exception e) {
    28. e.printStackTrace();
    29. }
    30. return o;
    31. }
    32. @Override
    33. public boolean supports(MethodParameter returnType, Class> converterType) {
    34. return true;
    35. }
    36. }

    37.  

    38. 相关阅读:
      Android->layer-list画对号画叉号画箭头画进度条
      Shell变量与赋值、变量运算、特殊变量、重定向与管渠
      HTTP1.0,1.1,2.0
      React(react18)中组件通信03——简单使用 Context 深层传递参数
      加速度速度位移的计算
      【小白学机器学习13】一文理解假设检验的反证法,H0如何设计的,什么时候用左侧检验和右侧检验,等各种关于假设检验的基础知识
      异常与错误处理高级用法
      Python的一个图片识别工具-PyTesseract(Win10)
      跳槽有技巧?超强测试开发面试经验等你pick
      用于新能源汽车三电系统测试的格雷希尔快速密封连接器的优选方案
    39. 原文地址:https://blog.csdn.net/u011998957/article/details/126765592