在 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[]
格式;