在Web应用中,确保前后端之间的数据传输安全是非常重要的。这通常涉及到使用HTTPS协议、数据加密、令牌验证等安全措施。本文通过将前后端之间的传输数据进行加密,用于在Spring Boot应用中实现前后端传输加密设计。
即使使用了HTTPS,也可能需要在应用层对数据进行额外的加密。这可以通过以下方式实现:
对称加密:
加密解密是同一个密钥,速度快,数据接收方需要公布其私钥给数据传输方进行数据加密,安全性完全依赖于该密钥。适合做大量数据或数据文件的加解密。
使用AES、DES等对称加密算法对敏感数据进行加密和解密。
非对称加密:
加密用公钥,解密用私钥。公钥和私钥是成对的(可借助工具生成,如openssl等),即用公钥加密的数据,一定能用其对应的私钥解密,能用私钥解密的数据,一定是其对应的公钥加密。对大量数据或数据文件加解密时,效率较低。数据接收方需公布其公钥给数据传输方,私钥自己保留,安全性更高。
混合加密
使用非对称加密算法交换对称加密的密钥(会话密钥),然后使用会话密钥进行实际的数据加密和解密。
这里就赘述介绍每种加密的实现方式和原理。
如果数据传输较大,密钥不需要进行网络传输,数据不需要很高的安全级别,则采用对称加密,只要能保证密钥没有人为外泄即可;
如果数据传输小,而且对安全级别要求高,或者密钥需要通过internet交换,则采用非对称加密;
本文采用了两者结合的方式(混合加密模式),这样是大多数场景下采用的加密方式。加密时序图如下所示:
通过使用对称加密(AES) 和 非对称加密(RSA) 的方式来实现对数据的加密;
即通过对称加密进行业务数据体的加密,通过非对称加密进行对称加密密钥的加密;
它结合了对称加密的高效性 和 非对称加密的安全性。
注意事项:
确保RSA公钥在传输过程中是安全的,因为任何拥有这个公钥的人都可以用它来加密AES密钥,但只有拥有私钥的人才能解密它。
确保在加密和解密过程中使用安全的加密库和最新的加密算法标准。
这种混合加密模式提供了安全性和效率之间的平衡。对称加密(如AES)用于加密大量数据,因为它通常比非对称加密更快。而非对称加密(如RSA)用于加密密钥,因为它提供了更强的安全性,特别是当密钥需要在不安全的通道上传输时。
封装AESUtil工具类时 pom.xml 中运用到的依赖:
<dependency > <groupId > cn.hutoolgroupId > <artifactId > hutool-allartifactId > <version > 5.8.18version >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 { // 选择生成数字还是字符 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 { /* * 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到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[i] = (byte ) Integer.parseInt(s.substring(2 *i,2 *i+2 ),16 ); } return bytes; } }
AES加解密工具类方法代码解析(为了方便自己理解和使用,有必要简单分类记录一下工具类中的方法接口):
AESUtil工具类中的方法封装的比较杂乱,通过梳理之后更加能理清每个方法的具体用法和功能!
生成AES密钥的方法:
该工具类中总共封装了两种 生成 AES密钥的方法
String getAESKey(int length)
和
String generateRandomAesKeyWithBase64()
getAESKey
方法生成的密钥是 由数字(0-9)、小写字母、大写字母随机组成的普通字符串;
generateRandomAesKeyWithBase64()
方法生成的密钥是 通过
javax.crypto.KeyGenerator
密钥生成器生成
Byte[]
类型的数据 在将该
byte[]
转换为 Base64编码格式。
AES加密数据方法:
1.AES工具类封装的加密数据方法有以下几种
byte[] encrypt(byte[] data,byte[] aesKey)
、
String encryptToBase64(String data,String aeskey)
和
String encryptWithKeyBase64(String data,String aesKey)
共三种加密方式:
注:其实其余加密方法都是基于该方法进行封装的。也可以根据自己需求来调整,注意区别在于传入的数据格式有所区别!!
2.
String encryptToBase64(String data,String aeskey)
该AES加密方法是通过传入加密数据的字符串,同时传入字符格式的AES密钥Key(通过getAESKey生成的密钥);方法内部会将传入进来的 待加密数据 data 和 aesKey密钥转换为
byte[]
格式,然后在调用第一种加密方法;最后生成的加密数据
byte[]
也会在内部自动转换为Base64编码格式 然后返回。
3.
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)
三种方式。
该三种方式分别与上面三种加密方式是对应的。需要注意传入的数据封装格式就行。
第一种解密方法就需要传入 加密后数据格式
byte[]
,密钥格式
byte[]
第二种解密方法对应于上面的第二种加密方法。需要传入的加密数据为Base64编码格式( 通过加密方法生成
byte[]
后 在转换为 Base64格式 ),需要传入AES密钥格式就为普通字符串格式(通过
String getAESKey(int length)
方法生成的密钥)。
第三种解密方法,需要传入的待解密数据 为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[]。
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 { /* * 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到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[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 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 map,boolean allowValueNull) { List 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)
byte [] encryptByPublicKey(byte [] data,String publicKey,int length)
RSAUtil工具类中的加密接口就只有一个,最终加密后的数据会以字节数组
byte[]
格式返回。最终用户想将密文以什么形式传输都可以(
String()
字符串形式、Base64编码格式、HexString十六进制字符串形式 )。
byte [] decryptByPrivateKey(byte [] data,String privateKey,int length)
在解码数据时,必须将密文数据根据对应的数据格式转换为
byte[]
后传入解码方法。
byte [] decryptBASE64(String src)String encryptBASE64 (byte [] src)
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
依托与SpringBoot进行开发,在后台中需要解密的请求接口,是采用了FIlter来实现解密操作。
采用 FIlter 来对加密数据进行解密的好处之一是:Filter 获取到参数后,可以将密文参数解密之后,重新重写请求参数。这样在Controller层处理业务逻的接口可以按照正常方式进行开发,
@RequestBody
、
@RequestParam
等注解都能正常使用。
在接口调用连接中,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 */ @Slf 4jpublic class RequestWrapper extends HttpServletRequestWrapper { /** * 存储Body数据 * */ private byte [] body; //============ /** * 保存原始Request对象,当请求为 MultipartRequest 文件上传类的请求操作 */ private HttpServletRequest request; /** * 额外参数可以加到这个里面 重写getParameter() 方法 ,从而使请求中不存在的参数,通过该Map集合中获取!! */ private Map 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 parameterMap = new LinkedHashMap<>(); Enumeration 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 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(); } @Override public BufferedReader getReader () throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream () throws IOException { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public boolean isFinished () { return false ; } @Override public boolean isReady () { return false ; } @Override public void setReadListener (ReadListener readListener) { } @Override public 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 */ @Override public 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. */ @Override public Map getParameterMap() { // 需要将原有的参数加上新参数 返回 Map 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 */ @Override public 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. */ @Override public Enumeration getParameterNames () { Enumeration parameterNames = super .getParameterNames(); Set names = new LinkedHashSet<>(); if (parameterNames !=null ){ while (parameterNames.hasMoreElements()){ names.add(parameterNames.nextElement()); } } // 添加后期设置的参数Map if (!this .parameterMap.isEmpty()){ names.addAll(this .parameterMap.keySet()); } return Collections.enumeration(names); } /** * 设置参数map * @param json2map */ public void setParameterMap (Map json2map) { if (json2map != null && !json2map.isEmpty()) { for (String key : json2map.keySet()){ //获取map中对应key的value Object 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
json2map)
在解密Filter中,将Get请求中地址上的加密数据进行解密之后,调用该方法就将解密后的Get请求数据封装到
requestWrapper
对象中的Map集合中实现后续调用
request.getParameter()
方法时能够获取到解密后的参数。
重写父类
getParameter(String name)
、
Map
getParameterMap()
、
String[] getParameterValues(String name)
等方法,实习在
getRequestParameter()
数据时,也能够在
requestWrapper
对象中的Map集合中获取参数数据。
这里封装的解密参数过滤器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 */ @Slf 4jpublic class DecryptReplaceStreamFilter implements Filter { /* * AKS(Authentication Key Management System),采用无明文密钥的方式对数据进行加密保护,加解密运算由统一的安全计算中心完成,AKS系统以接口的方式为对各个业务线提供加密解服务。 * 各个系统不再使用明文密钥,只使用密钥别名,调用简单的加解密函数,完成对数据的保护。 * */ //请求头标签开关:是否需要加密解密 private static final String AKS_ENABLE = "aksEncrypt" ; //GET请求加密数据Key public static final String AKS_PARAMETER = "encryptData" ; private static final String METHOD_GET = "GET" ; private static final String METHOD_POST ="POST" ; //POST请求加密数据Key private static final String AKS_BODY ="content" ; //AES密钥Key private 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; @Override public 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 --> c String 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密钥KEy String aesKey = jsonObject.getString(AES_KEY); //RSAUtil解密出AES密钥Key try { 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 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_PARAMETER String 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 map = objectMapper.readValue(decryptData, Map.class ) ; //将aseKey数据也存入map额外参数中 map.put("aesKey" ,aesKey); requestWrapper.setParameterMap(map); } } filterChain.doFilter(requestWrapper,servletResponse); } @Override public void init (FilterConfig filterConfig) throws ServletException { Filter.super .init(filterConfig); } @Override public 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 { /** * 注册过滤器 * */ @Bean public FilterRegistrationBean AllFilterRegistration () { FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setFilter(decryptReplaceStreamFilter()); registration.addUrlPatterns("/*" ); registration.setName("APISecurityFilter" ); registration.setOrder(1