在系统中,由于涉及到敏感数据的存储,我们需要对其进行加密以保障安全性。比如手机号这种敏感数据,在存储进数据库前需要先进行加密处理。当我们需要查询这些加密后的数据时,可以通过解密算法对其进行解密处理,并显示真实的手机号。这样的做法可以有效地保障敏感数据的安全,防止数据泄露,同时又能满足业务需求。在进行加密存储和解密处理的过程中,需要注意选择合适的加密算法和密钥管理方案,并严格控制访问权限,同时定期进行密钥更新等措施,以提高数据的安全性。
我们常见的加密算法分为对称加密、非对称加密
- 对称加密算法:使用同一个密钥进行加密和解密,如DES、AES等。
- 非对称加密算法:使用公私钥对进行加密和解密,如RSA、ECC等。
因为我们加密的手机号是要可逆的,所以我们选择对称加密算法AES,首先我们先看一个示例:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* 使用了 AES 加密算法和 CBC 模式,并使用一个固定的 KEY 和 IV
**/
public class AESEncryptor {
private static final String KEY = "0123456789abcdef";
private static final String IV = "abcdef0123456789";
public static String encrypt(String data) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] encrypted = cipher.doFinal(data.getBytes());
return bytesToHex(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String decrypt(String data) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decrypted = cipher.doFinal(hexToBytes(data));
return new String(decrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
}
编写测试方法
@Test
public void test(){
String phoneNumber = "13812345678";
String encryptedPhoneNumber = AESEncryptor.encrypt(phoneNumber);
System.out.println("加密后的手机号:" + encryptedPhoneNumber);
String decryptedPhoneNumber = AESEncryptor.decrypt(encryptedPhoneNumber);
System.out.println("解密后的手机号:" + decryptedPhoneNumber);
}
输出结果
加密后的手机号:91b572f017c3d983b175481c276ad4b0
解密后的手机号:13812345678
在上面的例子中我们使用AES加密算法并使用一个固定的 KEY 和 IV对手机号进行加解密,在AES加密算法和CBC模式中,KEY指的是密钥(Key),是对称加密算法中用于加密和解密的密钥,长度通常为128位、192位或256位。而IV则是初始化向量(Initialization Vector)的缩写,是一段固定长度的随机数,在加密过程中与明文一起参与异或运算,用来增加密码强度,并防止同样的明文经加密后得到相同的密文。IV长度通常要求为128位,且必须与密钥的长度相同。
具体来说,在CBC模式下,每个明文块先与前一个密文块进行异或操作,然后再使用AES算法进行加密。为了确保每个数据块都有一个唯一的初始向量,初始向量通常需要在每次加密时都进行更新。因此,CBC模式中的IV必须是随机生成的,且在每次加密时都要不同。我们对之前的代码进行优化
在yml文件中配置
aes:
key: 0123456789abcdef
我们新建一个工具类用来生成随机的初始化向量
import javax.xml.bind.DatatypeConverter;
import java.security.SecureRandom;
/**
* @Author
* @Date
* @Description
**/
public class InitializationVectorUtils {
/**
* 初始化向量的长度为16字节
*/
private static final int IV_LENGTH_BYTE = 16;
/**
* 获取随机的初始化向量
*
* @return 随机初始化向量
*/
public static String generateRandomIV(){
// 实例化安全随机数生成器
SecureRandom random = new SecureRandom();
// 创建一个字节数组来保存初始化向量
byte[] iv = new byte[IV_LENGTH_BYTE];
// 产生随机的初始化向量
random.nextBytes(iv);
// 将字节数组转换为十六进制字符串并返回
return DatatypeConverter.printHexBinary(iv);
}
}
新建加解密工具类
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 加解密工具类
* @Author
* @Date
* @Description
**/
public class AESEncryptor {
private final String key;
/**
* 十六进制字符表
* 用于将字节数组转换为十六进制字符串表示形式
*/
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
/**
* 构造 AES 加密器,使用指定的密钥
*
* @param key 密钥
*/
public AESEncryptor(final String key) {
this.key = Objects.requireNonNull(key, "Key must not be null");
}
/**
* 使用 AES 加密算法对字符串进行加密
*
* @param data 待加密字符串
* @param iv 16 位的初始化向量
* @return 加密后的字符串,以 16 进制形式表示
*/
public String encrypt(final String data, final String iv) throws Exception {
Objects.requireNonNull(data, "Data must not be null");
Objects.requireNonNull(iv, "IV must not be null");
//将 16 进制字符串转化为字节数组
final byte[] ivBytes = hexToBytes(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encrypted);
}
/**
* 将 16 进制字符串转化为字节数组
*
* @param hex 16 进制字符串
* @return 转化后的字节数组
*/
private static byte[] hexToBytes(final String hex) {
Objects.requireNonNull(hex, "Hex string must not be null");
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
/**
* 将字节数组转化为 16 进制字符串
*
* @param bytes 字节数组
* @return 转化后的 16 进制字符串
*/
private static String bytesToHex(final byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_ARRAY[v >>> 4];
hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
/**
* 使用 AES 解密算法对字符串进行解密
*
* @param data 待解密的字符串,以 16 进制形式表示
* @param iv 16 位的初始化向量
* @return 解密后的字符串
*/
public String decrypt(final String data, final String iv) throws Exception {
Objects.requireNonNull(data, "Data must not be null");
Objects.requireNonNull(iv, "IV must not be null");
//将 16 进制字符串转化为字节数组
final byte[] ivBytes = hexToBytes(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decrypted = cipher.doFinal(hexToBytes(data));
return new String(decrypted, StandardCharsets.UTF_8);
}
}
新建加密器配置类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AES 加密器配置类
*
* @Author
* @Date
* @Description
**/
@Configuration
public class AESEncryptorConfig {
/**
* 读取 application.yml 文件中的 aes.key 属性,用于构造 AES 加密器实例
*/
@Value("${aes.key}")
private String key;
/**
* 创建 AES 加密器实例,并将其注册为 Spring Bean
*
* @return AES 加密器实例
*/
@Bean
public AESEncryptor aesEncryptor() {
return new AESEncryptor(key);
}
}
在我们要使用的地方注入加解密类
@Autowired
private AESEncryptor aesEncryptor;
@ApiOperation(value = "这是一个测试")
@GetMapping("/test")
public Object test() {
try {
String phoneNumber = "13812345678";
String iv = InitializationVectorUtils.generateRandomIV();
System.out.println("iv: " + iv);
String encryptedPhoneNumber = aesEncryptor.encrypt(phoneNumber,iv);
System.out.println("加密后的手机号:" + encryptedPhoneNumber);
String decryptedPhoneNumber = aesEncryptor.decrypt(encryptedPhoneNumber,iv);
System.out.println("解密后的手机号:" + decryptedPhoneNumber);
System.out.println( decryptedPhoneNumber.equals(phoneNumber));
} catch (Exception e) {
e.printStackTrace();
}
return 1;
}
我们请求两次输出结果如下,如果我们使用随机的初始化向量一定要保存下来否则无法解密
iv: BA0997F2D82F2D709F32A5C72B3B3A16
加密后的手机号:FE60403C6A52694D867CCF251FECA1D1
解密后的手机号:13812345678
true
iv: 2815EE7DECBF89192FC4ABC87FA015AC
加密后的手机号:9C2FAFECB50C899BBAEF1EE5A4B2195B
解密后的手机号:13812345678
true