SpringBoot实现前后端传输加密设计

在Web应用中,确保前后端之间的数据传输安全是非常重要的。这通常涉及到使用HTTPS协议、数据加密、令牌验证等安全措施。本文通过将前后端之间的传输数据进行加密,用于在Spring Boot应用中实现前后端传输加密设计。

一、数据加密方案

即使使用了HTTPS,也可能需要在应用层对数据进行额外的加密。这可以通过以下方式实现:

  • 对称加密:加密解密是同一个密钥,速度快,数据接收方需要公布其私钥给数据传输方进行数据加密,安全性完全依赖于该密钥。适合做大量数据或数据文件的加解密。
    • 使用AES、DES等对称加密算法对敏感数据进行加密和解密。
    • 前后端需要共享一个密钥(key)用于加密和解密。
    • 密钥的管理和传输需要特别注意安全性。
  • 非对称加密:加密用公钥,解密用私钥。公钥和私钥是成对的(可借助工具生成,如openssl等),即用公钥加密的数据,一定能用其对应的私钥解密,能用私钥解密的数据,一定是其对应的公钥加密。对大量数据或数据文件加解密时,效率较低。数据接收方需公布其公钥给数据传输方,私钥自己保留,安全性更高。
    • 使用RSA、ECC等非对称加密算法。
    • 私钥用于加密数据,公钥用于解密数据。
    • 公钥可以公开,而私钥需要安全存储。
  • 混合加密
    • 结合使用对称加密和非对称加密。
    • 使用非对称加密算法交换对称加密的密钥(会话密钥),然后使用会话密钥进行实际的数据加密和解密。

这里就赘述介绍每种加密的实现方式和原理。

1.1 数据加密实现方式

如果数据传输较大,密钥不需要进行网络传输,数据不需要很高的安全级别,则采用对称加密,只要能保证密钥没有人为外泄即可;
如果数据传输小,而且对安全级别要求高,或者密钥需要通过internet交换,则采用非对称加密;
本文采用了两者结合的方式(混合加密模式),这样是大多数场景下采用的加密方式。加密时序图如下所示:

通过使用对称加密(AES) 和 非对称加密(RSA) 的方式来实现对数据的加密;即通过对称加密进行业务数据体的加密,通过非对称加密进行对称加密密钥的加密;它结合了对称加密的高效性 和 非对称加密的安全性。

注意事项:

  • 确保RSA公钥在传输过程中是安全的,因为任何拥有这个公钥的人都可以用它来加密AES密钥,但只有拥有私钥的人才能解密它。
  • 确保在加密和解密过程中使用安全的加密库和最新的加密算法标准。
  • 定期更换密钥对和对称密钥,以降低密钥泄露的风险。

这种混合加密模式提供了安全性和效率之间的平衡。对称加密(如AES)用于加密大量数据,因为它通常比非对称加密更快。而非对称加密(如RSA)用于加密密钥,因为它提供了更强的安全性,特别是当密钥需要在不安全的通道上传输时。

1.2 AES加密工具类创建

  • 封装AESUtil工具类时 pom.xml 中运用到的 依赖:
<!--   hutool-all工具类依赖   -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.18</version>
</dependency>
  • AES加解密工具类 AESUtil 代码:
package com.example.api_security_demo.utils;import cn.hutool.core.codec.Base64;import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;/*** @ClassName : AESUtil* @Description : AES加密工具类* @Author : AD*/
public class AESUtil {public static final String CHAR_ENCODING = "UTF-8";/*** [常见算法]AES、DES、RSA、Blowfish、RC4 等等* [常见的模式] ECB (电子密码本模式)、CBC (密码分组链接模式)、CTR (计数模式) 等等* [常见的填充] NoPadding、PKCS5Padding、PKCS7Padding 等等** [AES算法]可以有以下几种常见的值:* AES:标准的AES算法。* AES/CBC/PKCS5Padding:使用CBC模式和PKCS5填充的AES算法。* AES/ECB/PKCS5Padding:使用ECB模式和PKCS5填充的AES算法。* AES/GCM/NoPadding:使用GCM模式的AES算法,不需要填充。* AES/CCM/NoPadding:使用CCM模式的AES算法,不需要填充。* AES/CFB/NoPadding:使用CFB模式的AES算法,不需要填充。* */public static final String AES_ALGORITHM = "AES";public static char[] HEXCHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};/*** Description: 随机生成 AESKey密钥** @param length 随机生成密钥长度* @return java.lang.String*/public static String getAESKey(int length) throws Exception{/** Random类用于生成伪随机数。* */Random random = new Random();StringBuilder ret = new StringBuilder();for(int i = 0; i < length; i++){// 选择生成数字还是字符boolean isChar = (random.nextInt(2) % 2 == 0);/* 0随机生成一个字符*/if (isChar){// 选择生成大写字母 / 小写字母int choice = (random.nextInt(2) % 2 == 0) ? 65 : 97;ret.append((char) (choice+random.nextInt(26)));/* 1随机生成一个数字 */}else{ret.append( random.nextInt(10));}}return ret.toString();}/*** Description: 加密** @param data 待加密数据内容* @param aesKey 加密密钥* @return byte[]*/public static byte[] encrypt(byte[] data,byte[] aesKey){if (aesKey.length != 16){throw new RuntimeException("Invalid AES key length (must be 16 bytes) !");}try{/** 创建一个SecretKeySpec对象来包装AES密钥。* 它使用了aesKey字节数组作为密钥,并指定算法为"AES"。* 这个对象用来提供对称加密算法的密钥。* */SecretKeySpec secretKey = new SecretKeySpec(aesKey, "AES");/** 获取SecretKeySpec对象中的编码形式,将其存储在encodedFormat字节数组中。* 这个编码形式可以被用来重新构造密钥。* */byte[] encodedFormat = secretKey.getEncoded();/** 使用encodedFormat字节数组创建了另一个SecretKeySpec对象secKey。* 这个对象也用来提供对称加密算法的密钥。* */SecretKeySpec secKey = new SecretKeySpec(encodedFormat, "AES");/** 使用Cipher类的getInstance()方法获取了一个Cipher对象(创建密码器)。* 这个对象用来完成加密或解密的工作。* */Cipher cipher = Cipher.getInstance(AES_ALGORITHM);/** 码调用init()方法来初始化Cipher对象(初始化)。* 它要求传入操作模式和提供密钥的对象,这里使用Cipher.ENCRYPT_MODE代表加密模式,以及之前创建的secKey对象作为密钥。* */cipher.init(Cipher.ENCRYPT_MODE,secKey);/** 用Cipher对象对data进行加密操作,得到加密后的结果存储在result字节数组中。* */byte[] result = cipher.doFinal(data);return result;}catch (Exception e){throw new RuntimeException(" encrypt fail! ",e);}}/*** Description: 解密** @param data 解密数据* @param aesKey 解密密钥* @return byte[]*/public static byte[] decrypt(byte[] data,byte[] aesKey){if (aesKey.length != 16){throw new RuntimeException(" Invalid AES Key length ( must be 16 bytes)");}try {SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES");byte[] encodedFormat = secretKeySpec.getEncoded();SecretKeySpec secKey = new SecretKeySpec(encodedFormat, "AES");/* 创建密码器 */Cipher cipher = Cipher.getInstance(AES_ALGORITHM);/* 初始化密码器 */cipher.init(Cipher.DECRYPT_MODE,secKey);byte[] result = cipher.doFinal(data);return result;}catch (Exception e){throw new RuntimeException(" Decrypt Fail !",e);}}/*** Description:加密数据,并转换为Base64编码格式!** @param data 待加密数据* @param aeskey 加密密钥* @return java.lang.String*/public static String encryptToBase64(String data,String aeskey){try {byte[] valueByte = encrypt(data.getBytes(CHAR_ENCODING), aeskey.getBytes(CHAR_ENCODING));/* 加密数据转 Byte[]--> 换为Base64 --> String */return Base64.encode(valueByte);}catch (UnsupportedEncodingException e){throw new RuntimeException(" Encrypt Fail !",e);}}/*** Description: 解密数据,将Basse64格式的加密数据进行解密操作** @param data* @param aeskey* @return java.lang.String*/public static String  decryptFromBase64(String data,String aeskey){try {byte[] originalData = Base64.decode(data.getBytes());byte[] valueByte = decrypt(originalData,aeskey.getBytes(CHAR_ENCODING));return new String(valueByte,CHAR_ENCODING);}catch (UnsupportedEncodingException e){throw new RuntimeException("Decrypt Fail !",e);}}/*** Description:加密数据,aesKey为Base64格式时,并将加密后的数据转换为Base64编码格式** @param data* @param aesKey* @return java.lang.String*/public static String encryptWithKeyBase64(String data,String aesKey){try{byte[] valueByte = encrypt(data.getBytes(CHAR_ENCODING), Base64.decode(aesKey.getBytes()));return Base64.encode(valueByte);}catch (UnsupportedEncodingException e){throw new RuntimeException("Encrypt Fail!",e);}}/*** Description: 解密数据,数据源为Base64格式,且 aesKey为Base64编码格式** @param data* @param aesKey* @return java.lang.String*/public static String decryptWithKeyBase64(String data,String aesKey){try {byte[] originalDate = Base64.decode(data.getBytes());byte[] valueByte = decrypt(originalDate,Base64.decode(aesKey.getBytes()));return new String(valueByte,CHAR_ENCODING);}catch (UnsupportedEncodingException e){throw new RuntimeException("Decrypt Fail !",e);}}/*** Description:通过密钥生成器生成一个随机的 AES 密钥,并将其以字节数组的形式返回。* 主要功能是生成并返回一组随机的密钥字节数组,这些字节数组可用于加密和解密数据。** @param* @return byte[]*/public static byte[] generateRandomAesKey(){KeyGenerator keyGenerator = null;try{/** KeyGenerator是Java Cryptography Architecture(JCA)提供的主要密钥生成器类之一,用于生成对称加密算法的密钥。* 获取一个用于生成AES算法密钥的KeyGenerator实例,以便在加密和解密操作中使用该密钥。* */keyGenerator = KeyGenerator.getInstance(AES_ALGORITHM);}catch (NoSuchAlgorithmException e){throw new RuntimeException("GenerateRandomKey Fail !",e);}/** SecureRandom 类提供了一种用于生成加密强随机数的实现。* */SecureRandom secureRandom = new SecureRandom();/** 初始化密钥生成器 keyGenerator。* 初始化密钥生成器时使用了 SecureRandom 实例,以确保生成的密钥具有足够的随机性。* */keyGenerator.init(secureRandom);/** 调用 generateKey() 方法,使用初始化后的 keyGenerator 生成密钥对象 key。* */Key key = keyGenerator.generateKey();//返回生成的密钥的字节数组表示。return key.getEncoded();}/*** Description: 通过密钥生成器生成一个随机的 AES 密钥,并转化为Base64格式** @param* @return java.lang.String*/public static String generateRandomAesKeyWithBase64(){return Base64.encode(generateRandomAesKey());}/* !!当GET请求进行加密时,地址上的加密参数就以16进制字符串的方式进行传输,否则特殊符号路径无法解析[ +、/、=]等Base64编码格式 *//*** Description: 从Byte[] 数组转 16进制字符串** @param b* @return java.lang.String*/public static String toHexString(byte[] b){/** 每个字节都可以用两个十六进制字符来表示,因此初始化的容量是字节数组长度的两倍。* */StringBuilder sb = new StringBuilder(b.length * 2);for (int i = 0; i<b.length ;i++){/** 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到StringBuilder中* */sb.append(HEXCHAR[(b[i] & 0xf0) >>> 4]);/** 取字节的低四位,找到对应的十六进制字符,并追加到StringBuilder中。* */sb.append(HEXCHAR[b[i] & 0x0f]);}return sb.toString();}/*** Description: 从16进制字符串转 byte[] 数组** @param s* @return byte[]*/public static final byte[] toBytes(String s){byte[] bytes;bytes = new byte[s.length() / 2];for (int i = 0; i < bytes.length ; i++){bytes[i] = (byte) Integer.parseInt(s.substring(2*i,2*i+2),16);}return bytes;}
}

AES加解密工具类方法代码解析(为了方便自己理解和使用,有必要简单分类记录一下工具类中的方法接口):
AESUtil工具类中的方法封装的比较杂乱,通过梳理之后更加能理清每个方法的具体用法和功能!

  • 生成AES密钥的方法:

该工具类中总共封装了两种 生成 AES密钥的方法 String getAESKey(int length)String generateRandomAesKeyWithBase64()

  1. getAESKey 方法生成的密钥是 由数字(0-9)、小写字母、大写字母随机组成的普通字符串;
  2. generateRandomAesKeyWithBase64() 方法生成的密钥是 通过 **javax.crypto.KeyGenerator 密钥生成器 **生成Byte[] 类型的数据 在将该 byte[] 转换为 **Base64编码 **格式。

image.png

  • AES加密数据方法:

AES工具类封装的加密数据方法有以下几种  byte[] encrypt(byte[] data,byte[] aesKey)String encryptToBase64(String data,String aeskey) 和  String encryptWithKeyBase64(String data,String aesKey) 共三种加密方式:

  1. byte[] encrypt(byte[] data,byte[] aesKey) 该AES加密方法为最基础的加密方式,需要传入加密数据的 byte[] 格式数据,以及 AES密钥的byte[] 格式数据。加密之后的数据会以 byte[]格式返回。

注:其实其余加密方法都是基于该方法进行封装的。也可以根据自己需求来调整,注意区别在于传入的数据格式有所区别!!

  1. String encryptToBase64(String data,String aeskey) 该AES加密方法是通过传入加密数据的字符串,同时传入字符格式的AES密钥Key(通过getAESKey生成的密钥); 方法内部会将传入进来的 待加密数据 data 和 aesKey密钥转换为 byte[] 格式,然后在调用第一种加密方法;最后生成的加密数据byte[] 也会在内部自动转换为Base64编码格式 然后返回。
  2. String encryptWithKeyBase64(String data,String aesKey) 该AES加密方法,需要传入 字符串形式的加密数据,以及 **Base64编码格式的AES密钥 (**该密钥主要是通过generateRandomAesKeyWithBase64() 方法生成的密钥数据 为Base64编码格式 **)。**加密方法内部在接收到 待加密数据后会自动转换为byte[]格式;在接收到Base64编码格式的AES密钥后,通过Base64.decode() 将其解码为 byte[]。然后在调用原始的加密方法对待加密数据进行加密操作。最终加密后的数据byte[] 通过 new String(valueByte,“UTF-8”) 的方式转换为字符串返回。

  • AES解密数据方法:

AES工具类中封装的解密方法,对应于加密方法:byte[] decrypt(byte[] data,byte[] aesKey) String decryptFromBase64(String data,String aeskey) String decryptFromBase64(String data,String aeskey)。三种方式。

该三种方式分别与上面三种加密方式是对应的。需要注意传入的数据封装格式就行。

  1. 第一种解密方法就需要传入 加密后数据格式 byte[],密钥格式 byte[]
  2. 第二种解密方法对应于上面的第二种加密方法。需要传入的加密数据为Base64编码格式( 通过加密方法生成byte[] 后 在转换为 Base64格式 ),需要传入AES密钥格式就为普通字符串格式(通过 String getAESKey(int length) 方法生成的密钥)。
  3. 第三种解密方法,需要传入的待解密数据 为Base64编码格式,需要传入的AES密钥也为Base64编码格式

  • String toHexString(byte[] b) 方法

该方法是将byte[] 字节数据转换为16进制的字符串数据,后续会利用到。 比如在Get请求种传输加密数据,如果前端加密后的数据需要放入地址中进行传输到后端;若采用Base64编码格式数加密数据进行传输时,加密内容会包含 +\= 三个符号,无法在地址中进行传输了。所有这里封装了该方法,通过调用该方法,将加密后的byte[] 字节数据数据转换为 HexString 16进制字符串格式(只包含了 0~9、a、b、c、d、e、f)。这样Get请求中的加密数据就可以通过地址进行传输了

  • byte[] toBytes(String s) 方法

该方法于 toHexString 方法相对应,将转换为HexString十六进制的字符串 还原为字节数据Byte[]。

1.3 RSA加密工具类创建

RSA加密工具类,同样引用了 hutool-all 依赖工具类。

package com.example.api_security_demo.utils;import cn.hutool.core.codec.Base64Encoder;import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;/*** @ClassName : RSAUtil* @Description : RSA加密工具类* @Author : AD*/
public class RSAUtil {/*** "SHA256withRSA" 是一种使用 SHA-256 哈希算法和 RSA 加密算法结合的数字签名算法。* 在这种算法中,数据首先会通过 SHA-256 进行哈希处理,得到一个固定长度的摘要,然后使用 RSA 私钥对这个摘要进行加密,从而生成数字签名。* */public static final String  ALGORITHM_SHA256WITHRSA = "SHA256withRSA";public static final String KEY_ALGORITHM ="RSA";//RSA最大加密明文大小public static final int MAX_ENCRYPT_BLOCK = 117;//RSA最大解密密文大小public static final int MAX_DECRYPT_BLOCK = 128;private static char[] HEXCHAR = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };/*** Description: 公钥分段加密** @param data 待加密源数据* @param publicKey 公钥(BASE64编码)* @param length 段长 1024长度的公钥最大取117* @return byte[]*/public static byte[] encryptByPublicKey(byte[] data,String publicKey,int length) throws Exception{/** 将BASE64编码格式 publicKey进行解码* */byte[] publicKeyByte = decryptBASE64(publicKey);/** 使用X509EncodedKeySpec类创建了一个X.509编码的KeySpec对象,并将publicKeyByte作为参数传入。* 将公钥 [字符串] 解码成 [公钥对象] ,以便用于加密数据。* */X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);/** 通过KeyFactory获取了RSA的实例* */KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);/** 调用generatePublic方法使用之前创建的X509EncodedKeySpec对象来生成公钥。* */Key generatePublicKey = keyFactory.generatePublic(x509EncodedKeySpec);/** 创建一个Cipher实例,它是用于加密或解密数据的对象。Cipher类提供了加密和解密功能,并支持许多不同的加密算法。* 在这里,getInstance 方法中传入了keyFactory.getAlgorithm()[获取与指定密钥工厂相关联的算法名称。],它用于获取与指定算法关联的 Cipher 实例。* */Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());/** 初始化 Cipher 对象。* 在初始化过程中,指定加密模式为 ENCRYPT_MODE,并传入了之前生成的公钥 generatePublicKey。* */cipher.init(Cipher.ENCRYPT_MODE,generatePublicKey);int inputLen = data.length;ByteArrayOutputStream out = new ByteArrayOutputStream();//段落起始位置int offSet = 0;byte[] cache;int i = 0;//对数据进行分段加密while (inputLen - offSet > 0){if (inputLen - offSet > length) {cache = cipher.doFinal(data,offSet,length);} else {cache = cipher.doFinal(data,offSet,inputLen-offSet);}out.write(cache,0,cache.length);i++;offSet = i * length;}byte[] encryptDate = out.toByteArray();out.close();return encryptDate;}/*** Description:** @param data 待解密数据* @param privateKey 私密(BUSE64编码)* @param length 分段解密长度 128* @return byte[]*/public static byte[] decryptByPrivateKey(byte[] data,String privateKey,int length) throws Exception{byte[] privateKeyByte = decryptBASE64(privateKey);PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);Key generatePrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());cipher.init(Cipher.DECRYPT_MODE,generatePrivateKey);int inputLen = data.length;ByteArrayOutputStream out = new ByteArrayOutputStream();int offSet = 0;byte[] cache;int i = 0;//对数据进行分段解密while (inputLen - offSet > 0){if (inputLen - offSet > length){cache = cipher.doFinal(data,offSet,length);} else {cache = cipher.doFinal(data,offSet,inputLen - offSet);}out.write(cache,0,cache.length);i++;offSet = i * length;}byte[] decryptData = out.toByteArray();out.close();return decryptData;}/*** Description: BASE64解码** @param src* @return byte[]*/public static byte[] decryptBASE64(String src){sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();try{return decoder.decodeBuffer(src);}catch (Exception ex){return null;}}/*** Description: BASE64编码** @param src* @return java.lang.String*/public static String encryptBASE64(byte[] src){sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();return encoder.encode(src);}/*** Description: 从Byte[] 数组转 16进制字符串** @param b* @return java.lang.String*/public static String toHexString(byte[] b){/** 每个字节都可以用两个十六进制字符来表示,因此初始化的容量是字节数组长度的两倍。* */StringBuilder sb = new StringBuilder(b.length * 2);for (int i = 0; i<b.length ;i++){/** 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到StringBuilder中* */sb.append(HEXCHAR[(b[i] & 0xf0) >>> 4]);/** 取字节的低四位,找到对应的十六进制字符,并追加到StringBuilder中。* */sb.append(HEXCHAR[b[i] & 0x0f]);}return sb.toString();}/*** Description: 从16进制字符串转 byte[] 数组** @param s* @return byte[]*/public static final byte[] toBytes(String s){byte[] bytes;bytes = new byte[s.length() / 2];for (int i = 0; i < bytes.length ; i++){bytes[i] = (byte) Integer.parseInt(s.substring(2*i,2*i+2),16);}return bytes;}/*** Description: 判断对象是否为null*/public static boolean isEmpty(Object str) {return (str == null || "".equals(str));}/*** RSA 公钥私钥生成器* */public static Map generateRandomToBase64Key() throws Exception{String KEY_ALGORITHM ="RSA";KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);//密钥位数keyPairGenerator.initialize(1024);//创建公钥/私钥KeyPair keyPair = keyPairGenerator.generateKeyPair();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();byte[] publicKeyEncoded = publicKey.getEncoded();byte[] privateKeyEncoded = privateKey.getEncoded();HashMap<String,String> map = new HashMap<>();map.put("publicKey", Base64Encoder.encode(publicKeyEncoded));map.put("privateKey",Base64Encoder.encode(privateKeyEncoded));return map;}/*** Description: 根据请求参数Map集合,排号顺序Sort,组装生成对应请求中的签名参数sign** @param map 请求参数Map集合* @param allowValueNull 是否允许map中的值为null; true允许:若允许为空则会出现a=&b=* @return java.lang.String*/public static  String  generateSortSign(Map<String,String> map,boolean allowValueNull){List<String> keys = new ArrayList<>(map.size());for ( String key : map.keySet() ){/** 排除下列参数数据* 1.不允许出现空value 且 map中为null 的键值对* 2.参数签名内容键值对* */if ( (!allowValueNull && isEmpty(map.get(key))) || "sign".equals(key) || "signValue".equals(key) ){continue;}keys.add(key);}/** sort静态方法用于按自然顺序或自定义顺序对List进行排序* */Collections.sort(keys);StringBuffer stringBuffer = new StringBuffer();boolean isFirst = true;for (String key : keys){if (isFirst){stringBuffer.append(key).append("=").append(map.get(key));isFirst = false;continue;}stringBuffer.append("&").append(key).append("=").append(map.get(key));}return stringBuffer.toString();}/*** Description: 用于生成数据的数字签名,并将签名数据转换为十六进制字符串格式返回** @param rawDate 签名裸数据* @param privateKey 私钥* @param algorithm 签名验算算法* @return java.lang.String*/public static String generateSign(byte[] rawDate,String privateKey,String algorithm) throws Exception{byte[] privateKeyBytes = decryptBASE64(privateKey);PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);PrivateKey generatePrivate = keyFactory.generatePrivate(pkcs8EncodedKeySpec);/** 使用指定的签名算法algorithm,通过Signature实例获取签名对象signature。* */Signature signature = Signature.getInstance(algorithm);/** 初始化签名对象,传入生成的私钥generatePrivate。* */signature.initSign(generatePrivate);/** 将要签名的裸数据rawData传入签名对象。* */signature.update(rawDate);/** 生成签名数据sign* */byte[] sign = signature.sign();return toHexString(sign);}/*** Description: 验证签名** @param data 请求数据* @param publicKey 公钥* @param sign 签名数据* @param algorithm 签名验算算法* @return boolean*/public static boolean verify(byte[] data,String publicKey,String sign,String algorithm) throws Exception{byte[] publicKeyBytes = decryptBASE64(publicKey);X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);PublicKey generatePublicKey = keyFactory.generatePublic(x509EncodedKeySpec);Signature signature = Signature.getInstance(algorithm);signature.initVerify(generatePublicKey);signature.update(data);return signature.verify( toBytes(sign) );}
}

RSAUtil加解密工具类方法代码解析(为了方便自己理解和使用,有必要简单分类记录一下工具类中的方法接口):

  • 生成密钥对方法:Map generateRandomToBase64Key()
  • byte[] 与 Base64编码互相转换的方法:

byte[] decryptBASE64(String src)
String encryptBASE64(byte[] src)

  • HexString 十六进制字符串 与 byte[] 字节数组 互相转换的方法:

String toHexString(byte[] b)
byte[] toBytes(String s)

  • RSA加密/解密方法:

byte[] encryptByPublicKey(byte[] data,String publicKey,int length)
RSAUtil工具类中的加密接口就只有 一个,最终加密后的数据会以字节数组 byte[] 格式返回。最终用户想将密文以什么形式传输都可以( String() 字符串形式、Base64编码格式、HexString十六进制字符串形式 )。
byte[] decryptByPrivateKey(byte[] data,String privateKey,int length)
在解码数据时,必须将密文数据根据对应的数据格式转换为 byte[] 后传入解码方法。

  • Base64格式编码解码方法:

byte[] decryptBASE64(String src)
String encryptBASE64(byte[] src)


二、解密传输数据实现方案

依托与SpringBoot进行开发,在后台中需要解密的请求接口,是采用了FIlter来实现解密操作。


采用 FIlter 来对加密数据进行解密的好处之一是:Filter 获取到参数后,可以将密文参数解密之后,重新重写请求参数。这样在Controller层处理业务逻的接口可以按照正常方式进行开发,@RequestBody、@RequestParam 等注解都能正常使用。

2.1 Request 流只能读取一次的问题

在接口调用连接中,request的请求流只能调用一次,处理之后,如果之后还需要用到请求流获取数据,就会发现数据为空。比如使用了filter或者aop在接口处理之前,获取了request中的数据,对参数进行了校验,那么之后就不能在获取request请求流了。

  • 解决办法:

继承HttpServletRequestWrapper,将请求中的流copy一份,复写getInputStream和getReader等方法供外部使用。每次调用后的getInputStream方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一致存在。通过HttpServletRequestWrapper可以获取到前端加密的请求参数,同时也可以将解密后的参数设置进去。
Post请求:采用Filter来实现 加密传输数据的解密功能,在解密对应request请求流中的数据之后。将解密后的数据替换至自定义封装的requestWrapper对象中 body中。
Get请求:地址栏中添加了加密数据,在Filter进行解密之后,会将请求数据存入自定义封装的 requestWrapper 对象中的 Map集合数据中。在重写父类的 getParament() 等方法。
这里需要特别注意;对于MultipartRequest请求如果不做处理HttpServletRequestWrapper中是获取不到参数的;

  • 自定义 RequestWrapper对象:
package com.example.api_security_demo.common.core.wrapper;import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartRequest;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;/*** @ClassName : RequestWrapper* @Description : 自定义Request,解决request请求流中的数据二次或多次使用问题*  继承HttpServletRequestWrapper,将请求体中的流copy一份,覆写getInputStream()和getReader()方法供外部使用。*  每次调用覆写后的getInputStream()方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一直存在,这样就实现了流的重复读取。* @Author : AD*/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {/*** 存储Body数据* */private byte[] body;//============/*** 保存原始Request对象,当请求为 MultipartRequest 文件上传类的请求操作*/private HttpServletRequest request;/*** 额外参数可以加到这个里面 重写getParameter() 方法 ,从而使请求中不存在的参数,通过该Map集合中获取!!*/private Map<String, String[]> parameterMap = new LinkedHashMap<>();/*** Description: requestWrapper 请求包装类的构造方法** @param request* @return*/public RequestWrapper(HttpServletRequest request)throws IOException{super(request);//[文件上传相关的操作]this.request = request;if(request instanceof MultipartRequest){// 如果是[文件上传类]请求this.parseBody(request);}else{//[普通请求类]将Body数据存储起来String bodyString = getBodyString(request);body = bodyString.getBytes(Charset.defaultCharset());}}/* [MultipartRequest 文件上传]相关操作接口 *//*** 如果是 MultipartRequest,需要解析参数信息*/private void parseBody(HttpServletRequest request) {Map<String,Object> parameterMap = new LinkedHashMap<>();Enumeration<String> parameterNames = request.getParameterNames();while(parameterNames.hasMoreElements()){String name = parameterNames.nextElement();String[] values = request.getParameterValues(name);parameterMap.put(name, (values !=null && values.length == 1) ? values[0] : values);}// 将解析出来的参数,转换成JSON并设置到body中保存this.body = JSONObject.toJSONString(parameterMap).getBytes(Charset.defaultCharset());}public void setBody(byte[] body){this.body = body;try {if(this.request instanceof MultipartRequest){//[文件上传请求相关的操作]//todo 将Json格式body数据转换为mp//this.setParameterMap(JsonUtil.json2map(body));String bodyStr = new String(body, "UTF-8");com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();Map<String, Object> bodyMap = objectMapper.readValue(bodyStr, Map.class);this.setParameterMap(bodyMap);}} catch (Exception e) {log.error("转换参数异常,参数:{},异常:{}",body, e);}}/*** Description:读取请全体Body中数据 [从 requestWrapper中读取]** @param* @return java.lang.String*/public String getBodyString(){final InputStream inputStream  =  new ByteArrayInputStream(body);return inputStreamToString(inputStream);}/*** Description: 读取请求体Body中数据 [从HttpServletRequest中读取]** @param request* @return java.lang.String*/public String getBodyString(final ServletRequest request){try {return inputStreamToString(request.getInputStream());}catch (IOException e){log.error("Read Request Body IO_Stream Fail !",e);throw new RuntimeException(e);}}/*** Description: 将inputStream流里面的数据读取出来并转换为字符串形式** @param inputStream* @return java.lang.String*/private String inputStreamToString(InputStream inputStream){StringBuilder sb = new StringBuilder();BufferedReader reader = null;try {reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));String line;while ((line = reader.readLine()) != null){sb.append(line);}}catch (IOException e){log.error("BufferedReader is Fail !",e);throw new RuntimeException(e);}finally {if (reader != null){try {reader.close();} catch (IOException e) {throw new RuntimeException(e);}}}return sb.toString();}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() throws IOException {return inputStream.read();}};}// [Get请求相关操作,封装Request.getParameter()中的相关参数 ]/*** Description: The default behavior of this method is to return getParameter(Stringname) on the wrapped request object.** @param name* @return java.lang.String* @date 2024-05-13*/@Overridepublic String getParameter(String name) {String result =  super.getParameter(name);// 如果参数获取不到则尝试从参数(自定义封装的存贮零时请求数据的集合)Map中获取,并且只返回第一个if(result==null && this.parameterMap.containsKey(name)){result = this.parameterMap.get(name)[0];}return result;}/*** The default behavior of this method is to return getParameterMap() on the* wrapped request object.*/@Overridepublic Map<String, String[]> getParameterMap() {// 需要将原有的参数加上新参数 返回Map<String,String[]> map = new HashMap<>(super.getParameterMap());for(String key: this.parameterMap.keySet()){map.put(key, this.parameterMap.get(key));}return Collections.unmodifiableMap(map);}/*** The default behavior of this method is to return* getParameterValues(String name) on the wrapped request object.** @param name*/@Overridepublic String[] getParameterValues(String name) {String[] result =  super.getParameterValues(name);if(result == null && this.parameterMap.containsKey(name)){result = this.parameterMap.get(name);}return result;}/*** The default behavior of this method is to return getParameterNames() on* the wrapped request object.*/@Overridepublic Enumeration<String> getParameterNames() {Enumeration<String> parameterNames = super.getParameterNames();Set<String> names = new LinkedHashSet<>();if(parameterNames !=null){while(parameterNames.hasMoreElements()){names.add(parameterNames.nextElement());}}// 添加后期设置的参数Mapif(!this.parameterMap.isEmpty()){names.addAll(this.parameterMap.keySet());}return Collections.enumeration(names);}/*** 设置参数map* @param json2map*/public void setParameterMap(Map<String, Object> json2map) {if(json2map != null && !json2map.isEmpty()) {for (String key : json2map.keySet()){//获取map中对应key的valueObject value = json2map.get(key);if(this.parameterMap.containsKey(key)){//如果额外参数HashLink中包含该参数,则在赋值加入到String[] 中String[] originalArray = this.parameterMap.get(key);int originalLength = originalArray.length;originalArray = Arrays.copyOf(originalArray,originalLength + 1);originalArray[originalLength] = String.valueOf(value);//this.parameterMap.put(key, Collection.add(this.parameterMap.get(key), value));this.parameterMap.put(key,originalArray);}else{this.parameterMap.put(key, new String[]{String.valueOf(value)});}}}}}

RequestWrapper类方法代码解析(为了方便自己理解和使用,有必要简单分类记录一下工具类中的方法接口):这里自定义封装的 RequestWrapper对象,继承 HttpServletRequestWrapper。

  • 通过重写 getReader() 、getInputStream() 等方法,从而可以实现通过request对象读取body数据时,能够直接获取到该对象中我们自己封装的用于存放请求体数据的属性byte[] body。
  • **void setBody(byte[] body) **方法可以用来给自定义封装的属性body进行赋值,在 解密请求数据的 FIlter中,解密了body数据后,通过调用该方法将解密后的body数据存入requestWrapper对象。这样在后续的业务操作中直接通过request对象获取到的body数据就是已经解密的数据。实现了业务无感知!
  • **void setParameterMap(Map<String, Object> json2map) ** 在解密Filter中,将Get请求中地址上的加密数据进行解密之后,调用该方法就将解密后的Get请求数据封装到 requestWrapper对象中的Map集合中实现后续调用request.getParameter() 方法时能够获取到解密后的参数。
  • 重写父类 getParameter(String name)、Map<String, String[]> getParameterMap()、 String[] getParameterValues(String name)等方法,实习在getRequestParameter()数据时,也能够在requestWrapper对象中的Map集合中获取参数数据。

2.2 过滤器Filter解密请求参数

这里封装的解密参数过滤器Filter中,首先需要通过请求头中的 aksEncrypt数据判断该请求是否为加密请求,如果不是则直接放行不做解密操作。如果需要解密的请求,首先判断请求类型在进行对应的解密处理。

Filter解密数据过滤器代码:

package com.example.api_security_demo.filter;import com.alibaba.fastjson2.JSONObject;
import com.example.api_security_demo.common.core.wrapper.RequestWrapper;
import com.example.api_security_demo.utils.AESUtil;
import com.example.api_security_demo.utils.RSAUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.ObjectUtils;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;/*** @ClassName : DecryptReplaceStreamFilter* @Description :Filter过滤器 解密请求参数。同时替换请求体,使后续操作无感知加密!!* @Author : AD*/@Slf4j
public class DecryptReplaceStreamFilter implements Filter {/** AKS(Authentication Key Management System),采用无明文密钥的方式对数据进行加密保护,加解密运算由统一的安全计算中心完成,AKS系统以接口的方式为对各个业务线提供加密解服务。* 各个系统不再使用明文密钥,只使用密钥别名,调用简单的加解密函数,完成对数据的保护。* *///请求头标签开关:是否需要加密解密private static final String AKS_ENABLE = "aksEncrypt";//GET请求加密数据Keypublic static final String AKS_PARAMETER = "encryptData";private static final String METHOD_GET = "GET";private static final String METHOD_POST ="POST";//POST请求加密数据Keyprivate static final String AKS_BODY ="content";//AES密钥Keyprivate static final String AES_KEY = "aesKey";/*** Feign是声明式Web Service客户端,它让微服务之间的调用变得更简单,类似controller调用service。* Feign内部调用时,请求头中标注请求源* */public static final String SOURCE_KEY = "api-source";public static final String SOURCE_VALUE = "inner-api";@Value("${Rsa.PrivateKey}")private String privateKey;@Value("${API.Security.enable}")private boolean securityEnable;@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {//转换为自己Wrapper,实现多次读写RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);String contentType = requestWrapper.getContentType();  //请求头中获取Content-Type数据String requestURI = requestWrapper.getRequestURI();//配置文件配置是否开启加解密功能if (!securityEnable){log.info("未开启接口安全加密传输! 无需解密请求参数!");filterChain.doFilter(requestWrapper,servletResponse);return;}//通过请求头参数判断该请求是否需要解密处理if (!needAks(requestWrapper)){log.info("请求:{},非加密请求,无需解密操作!",requestURI);filterChain.doFilter(requestWrapper,servletResponse);return;}/** [该功能暂时不用管,因为在请求头中不添加 aksEncrypt:true  键值对,请求接口时同样不会去进行解密请求数据的操作]* *///Feign服务端调用内部请求,按照不加密的逻辑放行[Feign是声明式Web Service客户端,它让微服务之间的调用变得更简单,类似controller调用service。]//前端 --> A --> b --> cString sourceKey = requestWrapper.getHeader(SOURCE_KEY);if (!ObjectUtils.isEmpty(sourceKey) && sourceKey.equals(SOURCE_VALUE) ){log.info("内部请求,无效加密解密接口数据!");filterChain.doFilter(requestWrapper,servletResponse);return;}/** POST请求进行解密工作* */if (requestWrapper.getMethod().equalsIgnoreCase(METHOD_POST)){//读取JSON请求体数据StringBuffer bodyInfo = new StringBuffer();String line = null;BufferedReader reader = null;reader = requestWrapper.getReader();while ( ( line = reader.readLine() ) != null ){bodyInfo.append(line);}//解密请求体数据JSONObject jsonObject = JSONObject.parseObject(bodyInfo.toString());log.info("Post请求:{},待解密请求参数:{}", requestURI, JSONObject.toJSONString(jsonObject));//获取通过AES加密之后的密文String content = jsonObject.getString(AKS_BODY);//获取通过RSA加密之后的AES密钥KEyString aesKey = jsonObject.getString(AES_KEY);//RSAUtil解密出AES密钥Keytry {aesKey = new String(RSAUtil.decryptByPrivateKey(RSAUtil.toBytes(aesKey),privateKey,RSAUtil.MAX_DECRYPT_BLOCK),"UTF-8");} catch (Exception e) {throw new RuntimeException(e);}/** 方式1.将解密之后的数据+aesKey [放入body中,弃:会影响body结构] ( 满足在AOP操作中对出参数据进行加密 ) 一并交给下游业务。*///JSONObject requestBody = JSONObject.parseObject(data);//requestBody.put(AES_KEY,aesKey);/** 方式2.将其放入[请求对象属性中中],不影响请求体结构!*///requestWrapper.setAttribute(AES_KEY,aesKey);/** 方式3.将其放入[放入RequestWrapper封装的额外参数Map中],不影响请求体结构!* */HashMap<String, Object> map = new HashMap<>();map.put(AES_KEY,aesKey);requestWrapper.setParameterMap(map);//AESUtil + aesKey 解密json数据String decryptData = AESUtil.decryptFromBase64(content, aesKey);log.info("Get请求:{},解密之后参数:{}", requestURI, decryptData);//重置Json请求体,保证下游业务无感知获取数据//requestWrapper.setBody(requestBody.toJSONString().getBytes());requestWrapper.setBody(decryptData.getBytes());}/** GET请求解密处理:[AES密钥Key放置在请求头中]* */else if (requestWrapper.getMethod().equalsIgnoreCase(METHOD_GET)){//Get请求中 获取到指定加密的参数 然后进行解密操作String encryptData = requestWrapper.getParameter(AKS_PARAMETER);log.info("Get请求:{},待解密请求参数:{}", requestWrapper.getRequestURI(), encryptData);// 先解密 存放在请求头中且经过RSA加密过的AES密钥String aesKey = requestWrapper.getHeader(AES_KEY);if (encryptData != null && !encryptData.isEmpty() && aesKey != null && !aesKey.isEmpty()){try {byte[] aesKeyByte = RSAUtil.decryptByPrivateKey(RSAUtil.toBytes(aesKey), privateKey, RSAUtil.MAX_DECRYPT_BLOCK);aesKey = new String(aesKeyByte,"UTF-8");System.out.println("aesKey.toString() = " + aesKey.toString());} catch (Exception e) {throw new RuntimeException(e);}//Get请求中的加密数据是以16进制字符串方式传输byte[] encryptDataByte = AESUtil.toBytes(encryptData);System.out.println("Arrays.toString(encryptDataByte) = " + Arrays.toString(encryptDataByte));// 解密数据操作 AKS_PARAMETERString decryptData = new String(AESUtil.decrypt(encryptDataByte,aesKey.getBytes("UTF-8")),"UTF-8");log.info("Get请求:{},解密之后参数:{}", requestURI, decryptData);//将GET请求中的Parameter参数赋值入RequestWrapper中封装的其它参数存放集合Map中com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();Map<String, Object> map = objectMapper.readValue(decryptData, Map.class);//将aseKey数据也存入map额外参数中map.put("aesKey",aesKey);requestWrapper.setParameterMap(map);}}filterChain.doFilter(requestWrapper,servletResponse);}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}@Overridepublic void destroy() {Filter.super.destroy();}/*** Description: 判断当前请求是否需要加密解密数据** @param request* @return boolean*/private boolean needAks(HttpServletRequest request){String enableAKS = request.getHeader(AKS_ENABLE);return (enableAKS != null && enableAKS.equalsIgnoreCase("true"))? true: false;}
}

代码解析:

  • 代码中通过三重方式放到是否需要解密请求接口。一个是配置文件中读取的开关配置、一个是通过请求头中是否开启加密传输的标识(也就是在发送加密接口时,都需要在请求头上封装该开关)、Feign服务端调用内部请求,按照不加密的逻辑放行。
  • 解密方式按照文章开头采用的方案,先用RSA密钥解密出随机的AES密钥,在通过密钥解密密文。
  • 对应Get请求和Post请求的解密方式两点区别。Post整个请求体都是加密参数即body数据通过Base64方式传输进来的;Get请求中的加密参数需要使用:encryptData,由于需要放入路径中,Base64编码格式中含有+\=等特殊符号无法进行传输和解析,所以Get请求中的加密数据是采用HexString格式进行传输的,在解密时也需要对应的方法进行转换为Byte[]后在进行解密操作。
  • 关于AES密钥的存放位置。Post请求中的AES密钥是存入body中存入过来的,而Get请求中的AES密钥是存放在请求头中传递过来的,所以在解密对应请求方式的密文时,注意AES密钥的获取方式。
  • 关键解密后的AES密钥,需要一并交给下游业务。满足在AOP操作中对出参数据进行加密,所以解密出来的AES密钥也需要存入requestWrapper对象的Map集合中,方便下游业务获取。

Filter过滤器注册配置类:
需要注意解密过滤器Filter 的优先级一点要设置为最高优先级registration.setOrder(1);,首先需要该过滤器对加密数据进行解密后在重新封装请求,才能使后续的业务数据能直接获取到解密后的数据,达到无感知的效果。

package com.example.api_security_demo.filter;import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;/*** @ClassName : WebAllHandlerConfig* @Description : 过滤器、监听器、拦截器 前置处理配置类* @Author : AD*/@Configuration
public class WebAllHandlerConfig {/*** 注册过滤器* */@Beanpublic FilterRegistrationBean AllFilterRegistration(){FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();registration.setFilter(decryptReplaceStreamFilter());registration.addUrlPatterns("/*");registration.setName("APISecurityFilter");registration.setOrder(1);return registration;}@Bean(name = "decryptReplaceStreamFilter")public Filter decryptReplaceStreamFilter(){return new DecryptReplaceStreamFilter();}}


三、响应数据加密实现方案

响应数据加密的实现方式是采用AOP来实现,可以对返回的结果对象进行处理,而Filter只能拿到Request与Response对象,处理不方便;
这里的响应数据加密方案就简单采用的是AES加密,通过前端传来的随机AES进行加密响应数据后在响应给前端!
这里响应加密的开启方式是通过自定义注解来实现的,创建自定义一个自定义注解,作为响应数据加密的切点,就实现了响应数据是否加密的开启。


3.1 自定义注解(开启加密的注解):

package com.example.api_security_demo.common.core.annotation;import java.lang.annotation.*;/*** @ClassName : ResponseEncrypt* @Description : 响应数据加密开启注解* @Author : AD*/@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//@Documented
public @interface ResponseEncrypt {
}

3.2 AOP实现响应数据加密功能:

需要注意,这里的响应加密AOP的优先级也要设置为最高@Order(value = 0),从而使最先开始的AOP能最后结束,这样才能保证 加密响应的AOP最终处理的响应数据是所以业务逻辑都处理结束后最终的响应结果,然后进行加密处理后响应给前端。

package com.example.api_security_demo.aop;import com.example.api_security_demo.common.R;
import com.example.api_security_demo.common.core.annotation.ResponseEncrypt;
import com.example.api_security_demo.utils.AESUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @ClassName : ResponseEncryptAOP* @Description : 传输加密模块AOP,对接口的出参进行加密,注意顺序不能乱,* 此AOP必须第一个执行,因为最先执行的最后结束,这样才能在各个AOP都执行完毕之后完成最后的加密* @Author : AD*/@Order(value = 0)
@Aspect
@Component
public class ResponseEncryptAop {@Pointcut("@annotation(com.example.api_security_demo.common.core.annotation.ResponseEncrypt)")public void point(){}/*** 环绕增强,加密出参* */@Around(value = "point() && @annotation(responseEncrypt)")public Object aroundEncrypt(ProceedingJoinPoint joinPoint, ResponseEncrypt responseEncrypt) throws Throwable {//返回的结合Object returnValue = null;//从上下文中提取HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();//业务执行返回结果returnValue = joinPoint.proceed();System.out.println("returnValue.toString() = " + returnValue.toString());//获取到前端传递过来的AES密钥,然后对响应数据进行AES加密操作String aesKey = request.getParameter("aesKey");System.out.println("aesKey = " + aesKey);String encryptToBase64Data = AESUtil.encryptToBase64(returnValue.toString(), aesKey);returnValue = R.ok(encryptToBase64Data);return returnValue;}
}



四、测试相关的类


4.1 测试类实体封装

package com.example.api_security_demo.controller;import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;/*** @ClassName : TbStudenEntity* @Description : 测试实体类* @Author : AD*/@Data
@Accessors(chain = true)
@ToString
public class TbStudentEntity {int id;String name;String sex;int age;
}

4.2 测试接口Controller层封装

package com.example.api_security_demo.controller;import com.example.api_security_demo.common.R;
import com.example.api_security_demo.common.core.annotation.ResponseEncrypt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;/*** @ClassName : TbStudentController* @Description : 前端控制器Controller测试类* @Author : AD*/@RestController
@RequestMapping("/demo")
@Slf4j
public class TbStudentController {@PostMapping("/encryptPost")@ResponseEncryptpublic R postDemo(@RequestBody TbStudentEntity tbStudentEntity,HttpServletRequest request){System.out.println("request.getParameter(\"aesKey\") = " + request.getParameter("aesKey"));//System.out.println("request.getAttribute(\"aesKey\") = " + request.getAttribute("aesKey"));System.out.println("tbStudentEntity = " + tbStudentEntity);return R.ok(tbStudentEntity.toString());}@GetMapping("/encryptGet")@ResponseEncryptpublic R postDemoGet(HttpServletRequest request,@RequestParam String id,@RequestParam String name,@RequestParam String aesKey ){System.out.println("id = " + id);System.out.println("name = " + name);System.out.println("aesKey = " + aesKey);System.out.println("request.getParameter(\"age\") = " + request.getParameter("age"));//System.out.println("request.getAttribute(DecryptReplaceStreamFilter.AKS_PARAMETER) = " + request.getAttribute(DecryptReplaceStreamFilter.AKS_PARAMETER));return R.ok();}
}

4.3 前端模拟生成加密数据

package com.example.api_security_demo.utils;import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;/*** @ClassName : UtilsTestClass* @Description : 工具类测试类* @Author : AD*/@Slf4j
public class UtilsTestClass {/*** 可以将其放入配置文件中进行读取* */String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI7HE0aW9ILCMkfoAJYmAG+5RBRhU2Itebf04GUtnYMuR6Rl1GJKec7JKuM/8xSindH4jn6Vz3oARTjbCn4CxjbtQPys5i8VeXxgzzqhE34LY0Rt62Qy8UVS113454DTwZZR9BjmPQSxMaftQHMgeDjXVwLUt0a6CmRiZKOjw8WQIDAQAB";String privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMjscTRpb0gsIyR+gAliYAb7lEFGFTYi15t/TgZS2dgy5HpGXUYkp5zskq4z/zFKKd0fiOfpXPegBFONsKfgLGNu1A/KzmLxV5fGDPOqETfgtjRG3rZDLxRVLXXfjngNPBllH0GOY9BLExp+1AcyB4ONdXAtS3RroKZGJko6PDxZAgMBAAECgYBRUHdkKcNypwI188gnhBuu18QhQpa1CRbPBI90ObWWLMqQvcdj6tO2y3t1au+9Z/FXXzrN+IC6apU1p2M2HaB4iPdW+2Vm0DaRN7XJPzBdJqVASYVJ2oWWLWCkG05mS2pAgcMlxV3TLen7iBFKTjgZhdLIal8JYgyi6XWdNlAtAQJBAPjCtpmXuAS9luyXo2ExEauKEC2uC06FeZqgM6u1rTqsqUE5kGpmJ1DiERuOcQxn8i+mlnJ1Urmw8ltMxmnJAiECQQDOxVbjUXWolwRF8lsOeZ9XGGc/45pE7J8DubEV0Ii1RVckMyTkXmgNAupE7Xq9cgNMpNBRUQEDNzSHhA8rqGM5AkBuwWrBac6RtcPLpRwl+s3uPTNE01fPZxgkYy1+Rw5QsG1PUAzfgooAthZ92Wa16lXnJ1mWrmvdp03Qnpc8pDVhAkBoqWD6vV/2D0L9eNh4cj2iY1rX7whGfRNcWmD1rtGUF94tF6pD4jl+5Ivaie6H+C8NW5uKnZsKmqX/NmxLZ/eZAkBDXRX7YgMG4PLXcjPT4ot6DEJDjuSVb1LhwucL6QFFYhZloH3/9XQS3T9XB/2F61tdh5Jd3FaCD7WeXJCzW+64";/*** Description: 测试获取密钥的方法*/@Testpublic void testGetASK() throws Exception {// AES密钥String aesKey = AESUtil.getAESKey(16);System.out.println("aesKey = " + aesKey);// AES Base64类型密钥String base64AesKey = AESUtil.generateRandomAesKeyWithBase64();System.out.println("base64AesKey = " + base64AesKey);//RSA 私钥公钥Map map = RSAUtil.generateRandomToBase64Key();System.out.println("map.get(\"privateKey\") = " + map.get("privateKey"));System.out.println("map.get(\"publicKey\") = " + map.get("publicKey"));}/*** Description: post请求加密传输,请求体body封装测试** @param* @return void*/@Testpublic void postRequestBodyEncryptTest() throws Exception {//自定义方式获取 aesKeyString aesKey = AESUtil.getAESKey(16);//内部方式获取 Base64AesKeyString base64AesKey = AESUtil.generateRandomAesKeyWithBase64();//创建请求体body数据 content//模拟业务数据Json,并用AES密钥Key进行加密Map<String, Object> dataMap = new HashMap<>();dataMap.put("id", 15);dataMap.put("name", "张三");dataMap.put("sex", "男");dataMap.put("age", 20);JSONObject dataJson = new JSONObject(dataMap);String contentJson = JSONObject.toJSONString(dataJson);//AES 加密业务数据String content = AESUtil.encryptToBase64(contentJson, aesKey);log.info("AES加密数据操作: \nAES加密之前的数据 = {} \nAES加密之后的数据 content= {}",contentJson,content);//RSA 加密AES密钥byte[] bytes = RSAUtil.encryptByPublicKey(aesKey.getBytes("UTF-8"), publicKey, RSAUtil.MAX_ENCRYPT_BLOCK);String encryptAesKey = RSAUtil.toHexString(bytes);log.info("RSA加密AES密钥操作: \nRSA加密之前的AES密钥= {} \nRSA加密之后的AES密钥 aesKey={}",aesKey,encryptAesKey);/*** 09:08:42.376 [main] INFO com.example.api_security_demo.utils.UtilsTestClass - AES加密数据操作:* AES加密之前的数据 = {"sex":"男","name":"张三","id":15,"age":20}* AES加密之后的数据 content= u/vRpwktLAo12ATyfa1rb14EHNftHfvhYEfy7r+DOJzO6jzXS4bwUJ0xNY8RJu8f* 09:08:42.380 [main] INFO com.example.api_security_demo.utils.UtilsTestClass - RSA加密AES密钥操作:* RSA加密之前的AES密钥= 9raG41FIE8uK7l3k* RSA加密之后的AES密钥 aesKey=c7eccef1d112075ee64eec65163b8b1dcb1a54ea6c8b51875174f6d34fc4ac7d50d2977b7519d275ee610d717d594228e132b053a70cdad9f925701a728ed794684d12097cfb8bea598c561393cb69de384b2ec83aa8ddb9a98a5adb3ed51ee1b9aaab2cf7bc5b49712a95e40ac4ea17421f5250d34b8e629e58db0b26e54b39* */}/*** Description: Get请求加密传输,请求体RequestParameter数据封装测试* 注意: 通过AES加密之后的数据 byte[] 需要转换为 HexString十六进制字符串后才能在 请求地址中传输,不能在用Base64编码格式的方式进行传输!*/@Testpublic void getRequestBodyEncryptTest() throws Exception {//自定义方式获取 aesKeyString aesKey = AESUtil.getAESKey(16);//内部方式获取 Base64AesKeyString base64AesKey = AESUtil.generateRandomAesKeyWithBase64();//创建请求体body数据 content//模拟业务数据Json,并用AES密钥Key进行加密Map<String, Object> dataMap = new HashMap<>();dataMap.put("id", 15);dataMap.put("name", "张三");dataMap.put("sex", "男");dataMap.put("age", 20);JSONObject dataJson = new JSONObject(dataMap);String contentJson = JSONObject.toJSONString(dataJson);//AES 加密业务数据byte[] contentByte = AESUtil.encrypt(contentJson.getBytes("UTF-8"), aesKey.getBytes("UTF-8"));String content = AESUtil.toHexString(contentByte);log.info("AES加密数据操作: \nAES加密之前的数据 = {} \nAES加密之后的Byte[]数据 = {}  \nAES加密后的数据转换为HexString十六进制字符串数据encryptData={}",contentJson,Arrays.toString(contentByte),content);//RSA 加密AES密钥byte[] bytes = RSAUtil.encryptByPublicKey(aesKey.getBytes("UTF-8"), publicKey, RSAUtil.MAX_ENCRYPT_BLOCK);String encryptAesKey = RSAUtil.toHexString(bytes);log.info("RSA加密AES密钥操作: \nRSA加密之前的AES密钥= {} \nRSA加密之后的AES密钥aesKey={}",aesKey,encryptAesKey);/*** 09:09:13.172 [main] INFO com.example.api_security_demo.utils.UtilsTestClass - AES加密数据操作:* AES加密之前的数据 = {"sex":"男","name":"张三","id":15,"age":20}* AES加密之后的Byte[]数据 = [73, 19, 107, 60, 29, 109, 100, 119, -81, -117, -84, -85, 19, 28, 86, 18, 123, 48, 58, 37, -28, -65, -93, -124, -50, 89, -10, -101, 48, 48, -104, -18, -109, 127, -19, 80, 62, -122, -80, -122, 94, 72, -16, -89, -1, -128, 22, -92]* AES加密后的数据转换为HexString十六进制字符串数据encryptData=49136b3c1d6d6477af8bacab131c56127b303a25e4bfa384ce59f69b303098ee937fed503e86b0865e48f0a7ff8016a4* 09:09:13.177 [main] INFO com.example.api_security_demo.utils.UtilsTestClass - RSA加密AES密钥操作:* RSA加密之前的AES密钥= 868E9FMA727S9W5q* RSA加密之后的AES密钥aesKey=2ab5531c7814201b4eaef3802ca883e79ffffd4c4ec32e698403189c0954718fd5cebd0ac5e66e856ec4f95a408442fc76276586a8fdb94c14c8f311f30ad061d6928315078736e6633113cdba255870a78e9077b2f18bdc4b2730804e5d6181df4b0ecf51597f71c8e0ccb89a5e160f1216a7bde5386b42171577db400d5a54* */}@Testpublic void testByteBase64String(){/** 创建字节数组的方式* */byte[] testBytes = new byte[]{0,1,2,3,4};/** 读取字节数组的方式:* */System.out.println("Arrays.toString(bytes1) = " + Arrays.toString(testBytes));//模拟业务数据Json,并用AES密钥Key进行加密Map<String, String> dataMap = new HashMap<>(2);dataMap.put("name", "张三");dataMap.put("age", "20");JSONObject dataJson = new JSONObject(dataMap);String jsonStr = JSONObject.toJSONString(dataJson);com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();try {Map<String, Object> map = objectMapper.readValue(jsonStr, Map.class);System.out.println("map.toString() = " + map.toString());System.out.println("map.get(\"name\") = " + map.get("name"));System.out.println("map.get(\"age\") = " + map.get("age"));} catch (JsonProcessingException e) {throw new RuntimeException(e);}byte[] jsonByte = dataJson.toString().getBytes();System.out.println("new String(jsonByte) = " + new String(jsonByte));//[两种方式不一样!]String hexStr = "0123456789abcdef";System.out.println("Arrays.toString(hexStr.getBytes()) = " + Arrays.toString(hexStr.getBytes()));System.out.println("Arrays.toString(RSAUtil.toBytes(hexStr)) = " + Arrays.toString(RSAUtil.toBytes(hexStr)));}/*** Description: 解密操作测试*/@Testpublic void testAesDecrypt() throws Exception {String encryptAesKey = "868dbf00b6849a1189f186f18bc98eca65981829e12d3bad21f4a64c139a6fe6953729e488af642cb5bf7104459a4fbf084bb536f251e2d9fa39747037715da6a73caf23e1d68bd5338d51dd207ebe9c4a72749d87d73eb96fc193adac45e6b8b6b7fcc211ee47efd0d54ea97dcfdbc221ac0bd7664d32becbdd654c3d9b2446";String encryptDate = "8ee240b806a571f0f7ef5568d9cf5e36d999686acabfa4d5425d73ef7e546c8c1e9147c084269a6884cfeebf6759bd60";//解密之后的AesKey 56X8817GRC2p33w0byte[] decryptAesKey = RSAUtil.decryptByPrivateKey(RSAUtil.toBytes(encryptAesKey), privateKey, RSAUtil.MAX_DECRYPT_BLOCK);String decryptAesKeyStr = new String(decryptAesKey, "UTF-8");System.out.println("decryptAesKeyStr = " + decryptAesKeyStr);//GET请求数据解密 [加密数据为HexStr十六进制字符串形式]byte[] encryptDataByte = AESUtil.toBytes(encryptDate);System.out.println("Arrays.toString(encryptDataByte) 加密数据Byte[]形式 = " + Arrays.toString(encryptDataByte));String decryptData = new String(AESUtil.decrypt(encryptDataByte,decryptAesKeyStr.getBytes("UTF-8")),"UTF-8");System.out.println("decryptData解密后的数据 = " + decryptData);/*  //POST请求数据解密 [加密数据为Base64编码格式]String decryptData2 = AESUtil.decryptFromBase64(encryptDate, decryptAesKeyStr);System.out.println("decryptData2 解密后的数据 = " + decryptData2);*//*** AES加密之前的数据 = {"sex":"男","name":"张三","id":15,"age":20}* AES加密之后的Byte[]数据 = [-114, -30, 64, -72, 6, -91, 113, -16, -9, -17, 85, 104, -39, -49, 94, 54, -39, -103, 104, 106, -54, -65, -92, -43, 66, 93, 115, -17, 126, 84, 108, -116, 30, -111, 71, -64, -124, 38, -102, 104, -124, -49, -18, -65, 103, 89, -67, 96]* AES加密后的数据转换为HexString十六进制字符串数据encryptData=8ee240b806a571f0f7ef5568d9cf5e36d999686acabfa4d5425d73ef7e546c8c1e9147c084269a6884cfeebf6759bd60* 16:48:25.095 [main] INFO com.example.api_security_demo.utils.UtilsTestClass - RSA加密AES密钥操作:* RSA加密之前的AES密钥= 8c0N9032214LJ139* RSA加密之后的AES密钥aesKey=868dbf00b6849a1189f186f18bc98eca65981829e12d3bad21f4a64c139a6fe6953729e488af642cb5bf7104459a4fbf084bb536f251e2d9fa39747037715da6a73caf23e1d68bd5338d51dd207ebe9c4a72749d87d73eb96fc193adac45e6b8b6b7fcc211ee47efd0d54ea97dcfdbc221ac0bd7664d32becbdd654c3d9b2446* */}/*** Description: 响应数据加密后,模拟前端进行解密操作*/@Testpublic void responseEncryptToDecrypt() throws UnsupportedEncodingException {String aesKeyPost = "9raG41FIE8uK7l3k";String aesKeyGet = "0303F71572405EF1";String encryptData = "cf156ddc9cd9fde2a8287fa3b8eadca258ce063130d29d7d9c1b949a4628c42e497b4e1db76244bdd075cb37b8ef0212";//解密后的数据String decryptData = "";//Base64编码格式的数据解密//decryptData = AESUtil.decryptFromBase64(encryptData, aesKeyGet);//HexString格式的数据转换后在解密byte[] decrypt = AESUtil.decrypt(AESUtil.toBytes(encryptData), aesKeyGet.getBytes("UTF-8"));decryptData = new String(decrypt, "UTF-8");System.out.println("decryptData = " + decryptData);}
}

4.4 模拟加密请求调用接口

4.4.1 Post请求的封装

  • 请求头中封装数据:

aksEncrypt: true 开启解密功能
image.png

  • 请求体Body(application/json格式)的封装:

content:通过随机AES密钥加密后的请求数据
aesKey: 通过RSA加密后的随机AES密钥
image.png

  • Post请求后响应数据

可以看出 请求数据解密后,通过 RequestWrapper重新封装后的请求数据,能直接通过@RequestBody 等注解直接获取到相请求体中的数据。 同时也可以通过 request.getParameter()中存放的参数。
image.png

4.4.2 Get请求的封装

  • Get请求的封装

image.png

  • Get请求响应结果:

image.png

可以看出 请求数据解密后,通过 RequestWrapper重新封装后的请求数据,能直接通过@RequestParam等注解直接获取到Get请求中的数据。 同时也可以通过 request.getParameter()中存放的参数。


[结束]:还有些地方封装的不是很恰当,欢迎各位大佬留言提意见!

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

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

相关文章

安防监控视频打手机检测算法核心技术打手机检测算法源码、模型简介

在数字化的今天&#xff0c;智能手机几乎已成为人们生活中不可或缺的一部分。然而&#xff0c;手机的广泛使用也带来了一些挑战&#xff0c;比如在公共场所、教育机构和工作环境中的手机干扰。为了解决这些问题&#xff0c;打手机检测算法应运而生&#xff0c;成为管理人员和机…

足底筋膜炎怎么治疗才能除根

一、足底筋膜炎的引起原因&#xff0c;足底筋膜炎是一种常见的足部疾病&#xff0c;其引起原因多样&#xff0c;主要包括以下几个方面&#xff1a; 1、过度使用&#xff1a;长时间站立、行走或跑步等活动&#xff0c;特别是当这些活动超出了足底筋膜的承受能力时&#xff0c;会…

毒枸杞事件启示录:EasyCVR视频AI智能监管方案如何重塑食品卫生安全防线

一、方案背景 近年来&#xff0c;食品安全问题频发&#xff0c;引发了社会各界的广泛关注。其中&#xff0c;毒枸杞事件尤为引人关注。新闻报道&#xff0c;在青海格尔木、甘肃靖远等地&#xff0c;部分商户为了提升枸杞的品相&#xff0c;违规使用焦亚硫酸钠和工业硫磺进行“…

Android Telephony总结

1、Telephony 业务介绍 Android telephony涉及较多模块 1.1、STK业务介绍 1.1.1、STK域选 1.1.2、是否支持STK Telephon STK-CSDN博客 1.1.3、STK应用的安装卸载 1.2、SS补充业务 1.3、通话业务 1.3.1、紧急号码 ECC 号码总结_ecc号码-CSDN博客 1.4、SMS 1.4.1 短信发送方式…

vue3 VueUse useElementVisibility 来监听某一个元素或者div是否在当前视口viewport中可见。

1、先上一个图&#xff1a; 2、安装vueuse/core pnpm add vueuse/core 3、新建一个组件&#xff1a; <script setup lang"ts"> import { ref, watch } from "vue"; import { useElementVisibility } from "vueuse/core";const target re…

备考MS office 二级

word 1、分页符 布局-分隔符-分节符-下一页&#xff1a;第二张可以不同纸张大小、方向等 2、调整宽度&#xff1a;新文字宽度&#xff08;字符宽度&#xff09; 中文版式 3、字符间距 4、文本转换为表格 1、把 || 替换为逗号&#xff0c;方便查找 5、首字下沉 插入 - 首字下沉…

便宜好用的云手机盘点

云手机作为一种新型远程计算服务&#xff0c;凭借其便利性、高效性和可扩展性&#xff0c;迅速成为了用户的热门选择。然而&#xff0c;面对市场上众多的云手机品牌&#xff0c;如何选择一款性价比高且体验良好的云手机&#xff1f;本文将为您盘点几款便宜好用的云手机产品。 雷…

Avalonia 播放 VLC 视频(Windows / Linux)

【演示效果】 一、开发步骤 1. 版本与引用类库 Avalonia 版本:11.0.11 Windows上只需要安装以下类库: LibVLCSharp 3.8.5 LibVLCSharp.Avalonia 3.8.5 VideoLAN.LibVLC.Windows 3.0.20 引用截图: 2. 前端代码 <UserControl xmlns="https://github.com/avaloni…

windows手工杀毒-寻找可疑进程之进程模块

上篇回顾&#xff1a;windows手工杀毒-寻找可疑进程之进程启动文件-CSDN博客 上篇我们介绍了如何通过进程启动文件寻找可疑进程&#xff0c;首先我们查看文件的数字签名&#xff0c;如果签名是合法的&#xff0c;且是正规公司的证书。基本可疑排除进程是可疑进程&#…

pnpm国内源设置

一、背景 在国内使用pnpm时&#xff0c;由于网络问题&#xff0c;经常会遇到速度慢或无法访问的问题。为了提高效率&#xff0c;可以将pnpm的源设置为国内的镜像源。以下是一些常用的国内pnpm镜像源以及如何设置它们的方法。 二、国内可用源 2.1 淘宝pnpm源 https://registry…

【全网最全】2024年数学建模国赛B题保奖思路+成品论文+matlab/python代码等(后续会更新

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击末文的卡片&#xff0c;那是获取资料的入口&#xff01; 一、问题重述 本题聚焦于某电子产品制造企业在生产过程中的决策问题。企业在生产中需采购 和使用两种关键零配件&#xff0c;并在装配过程中面临如何优化质…

2024年全国大学生数学建模竞赛(C题) 建模解析|农作物的种植策略|小鹿学长带队指引全代码文章与思路

我是鹿鹿学长&#xff0c;就读于上海交通大学&#xff0c;截至目前已经帮200人完成了建模与思路的构建的处理了&#xff5e; 本篇文章是鹿鹿学长经过深度思考&#xff0c;独辟蹊径&#xff0c;实现综合建模。独创复杂系统视角&#xff0c;帮助你解决国赛的难关呀。 完整内容可以…

SpringBoot依赖之Spring Boot DevTools热部署开发增效工具

摘要&#xff1a;Spring项目又大又重&#xff0c;依赖多&#xff0c;编译启动慢&#xff0c;怎么提高研发效率呢&#xff1f;方法之一热部署&#xff01; 概念 Spring Boot DevTools 依赖名称: Spring Boot DevTools功能描述: Provides fast application restarts, LiveRelo…

Milvus 向量数据库进阶系列丨构建 RAG 多租户/多用户系统 (下)

本系列文章介绍 在和社区小伙伴们交流的过程中&#xff0c;我们发现大家最关心的问题从来不是某个具体的功能如何使用&#xff0c;而是面对一个具体的实战场景时&#xff0c;如何选择合适的向量数据库解决方案或最优的功能组合。在 “Milvus 向量数据库进阶” 这个系列文章中&a…

天润融通解开售后维修的成本枷锁,提高维修服务效率

如今&#xff0c;企业客户服务在开展业务咨询和售后受理时&#xff0c;主要方式还是通过电话与在线方式进行。这种方式虽然方便&#xff0c;但是对于一些非常紧急的情况还是显得有些不够。 比如&#xff0c;虽然现在许多企业APP已经实现了一键咨询和一键报修&#xff0c;但当客…

cuda编程[1]:一二三维网格和块的核函数

目录 前言核函数一维二维三维结果分析 前言 所有的代码下载链接&#xff1a;code。以下代码展示了如何在 CUDA 中打印网格和线程的索引信息。代码包括一维、二维和三维的网格和块的设置&#xff0c;并定义了多个内核函数来输出当前的索引信息。 核函数 打印线程索引 __gl…

[MOCO v3] An Empirical Study of Training Self-Supervised Vision Transformers

1、目的 探索基于contrastive/Siamese范式&#xff08;而非masked auto-encoding范式&#xff09;和ViT结构&#xff08;而非卷积网络&#xff09;的自监督学习 2、方法 MoCo v3 ​​​​​​​ 1&#xff09;random data augmentation 2&#xff09;query encoder &a…

shell 学习笔记:数组

目录 1. 定义数组 2. 读取数组元素值 3. 关联数组 4. 在数组前加一个感叹号 ! 可以获取数组的所有键 5. 在数组前加一个井号 # 获取数组的长度 6. 数组初始化的时候&#xff0c;也可以用变量 7. 循环输出数组的方法 7.1 for循环输出 7.2 while循环输出 7.2.1 …

数据结构与算法 第10天(图的应用)

一、最小生成树 生成树&#xff1a;所有顶点均由边连接在一起&#xff0c;但不存在回路 一个图可以有多颗不同的生成树 生成树特点&#xff1a;生成树的顶点个数与图的顶点个数相同; 生成树是图的极小连通子图&#xff0c;去掉一条边则非连通, 一个有 n个顶点的连通图的生成…

idea插件开发之bean复制插件

背景 周末在家无事做&#xff0c;顺手开发了一个之前一直想要做的插件&#xff0c;那就是bean复制插件。 在项目中&#xff0c;由于代码分层设计&#xff0c;对于同样一个数据我们通常会定义不同层的实体&#xff0c;例如xxxEntity、xxxDTO、xxxVO等&#xff0c;这些不同的实…