点击“IT码徒”,关注,置顶公众号
每日技术干货,第一时间送达!在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()
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[]。
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
