一、核心方案设计:AES-256-GCM + MyBatis拦截
数字档案馆系统若仅依赖简单的Base64编码或AES-ECB模式,存在极大的安全隐患。ECB模式无法隐藏明文模式,相同的明文会产生相同的密文,极易被统计分析攻击。本文将采用AES-256-GCM算法,该算法提供认证加密功能,不仅能保证机密性,还能保证数据完整性,防止密文被篡改。
为了实现“零代码侵入”的落地效果,我们将利用MyBatis的TypeHandler机制。在数据写入数据库前自动加密,读取后自动解密,业务层无需修改任何逻辑,直接操作明文字段即可。
二、项目环境与Maven依赖配置
本方案基于Spring Boot 2.x/3.x环境,无需引入额外的重型加密库,直接使用JDK自带的JCE(Java Cryptography Extension)。请确保你的JDK版本为1.8及以上。在pom.xml中添加以下必要依赖:
```xml
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.2
com.baomidou
mybatis-plus-boot-starter
3.5.2
org.projectlombok
lombok
1.18.24
provided
```
三、生成高强度的加密密钥
AES-256要求密钥长度必须为32字节(256位)。绝对禁止在代码中硬编码密钥字符串。我们需要生成一个随机的32字节密钥,并将其配置在application.yml或环境变量中。运行以下Java代码生成密钥:
```java
import javax.crypto.KeyGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class SecretKeyGenerator {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 初始化 KeyGenerator,指定使用 AES 算法,密钥长度 256
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256);
byte[] keyBytes = keyGenerator.generateKey().getEncoded();
// 将二进制密钥转换为 Base64 字符串,方便配置
String base64Key = Base64.getEncoder().encodeToString(keyBytes);
System.out.println("请将此密钥配置到 application.yml 的 encrypt.secret-key 字段中:");
System.out.println(base64Key);
}
}
```
运行上述代码,复制输出的Base64字符串。例如:Xm8s9N...(此处省略)...=。
四、编写AES-GCM加解密工具类
创建一个工具类AesGcmUtil.java。GCM模式需要一个IV(初始化向量),IV不需要保密,但每次加密必须唯一。为了方便数据库存储,我们将IV和密文拼接在一起存储(格式:IV + Ciphertext)。
```java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Base64;
public class AesGcmUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128; // 必须是 128, 120, 112, 104, 96, 64, 或 32
private static final int GCM_IV_LENGTH = 12; // 推荐使用 12 字节的 IV
/
加密方法
@param plaintext 明文
@param base64Key Base64编码的密钥
@return Base64编码的密文(包含IV)
/
public static String encrypt(String plaintext, String base64Key) throws Exception {
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}
// 1. 恢复密钥
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
SecretKeySpecKeySpec = new SecretKeySpec(keyBytes, ALGORITHM);
// 2. 生成随机 IV
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(iv);
// 3. 初始化 Cipher
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
// 4. 加密
byte[] ciphertext = cipher.doFinal(plaintext.getBytes());
// 5. 组合 IV 和 密文 以便存储
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + ciphertext.length);
byteBuffer.put(iv);
byteBuffer.put(ciphertext);
// 6. 返回 Base64 编码
return Base64.getEncoder().encodeToString(byteBuffer.array());
}
/
解密方法
@param encryptedText Base64编码的密文(包含IV)
@param base64Key Base64编码的密钥
@return 明文
/
public static String decrypt(String encryptedText, String base64Key) throws Exception {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
// 1. 恢复密钥
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
// 2. 解码 Base64 密文
byte[] decodedMessage = Base64.getDecoder().decode(encryptedText);
// 3. 分离 IV 和 密文
ByteBuffer byteBuffer = ByteBuffer.wrap(decodedMessage);
byte[] iv = new byte[GCM_IV_LENGTH];
byteBuffer.get(iv);
byte[] ciphertext = new byte[byteBuffer.remaining()];
byteBuffer.get(ciphertext);
// 4. 初始化 Cipher
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
// 5. 解密
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
}
}
```
五、实现MyBatis透明加密TypeHandler

这是实现“零门槛”落地的核心。我们需要自定义一个TypeHandler,让MyBatis在将Java String存入数据库时调用encrypt,从数据库取出时调用decrypt。
创建类CryptoTypeHandler.java:
```java
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 指定该 Handler 处理 String 类型的字段
@Component
@MappedTypes(String.class)
public class CryptoTypeHandler extends BaseTypeHandler
{
// 从配置文件中读取密钥,使用 @Value 注入
// 注意:如果在非 Spring 环境下使用 MyBatis,需通过其他方式获取配置
private static String secretKey;
@Value("${encrypt.secret-key}")
public void setSecretKey(String key) {
CryptoTypeHandler.secretKey = key;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
try {
// 存入数据库时:加密
String encrypted = AesGcmUtil.encrypt(parameter, secretKey);
ps.setString(i, encrypted);
} catch (Exception e) {
throw new SQLException("Failed to encrypt parameter: " + parameter, e);
}
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return decryptValue(value);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return decryptValue(value);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return decryptValue(value);
}
private String decryptValue(String value) {
if (value == null || value.isEmpty()) {
return value;
}
try {
// 从数据库读取时:解密
return AesGcmUtil.decrypt(value, secretKey);
} catch (Exception e) {
// 防止解密失败导致系统崩溃,可根据需求记录日志或抛出异常
// 此处简单返回原值或抛出 RuntimeException
throw new RuntimeException("Failed to decrypt value from DB", e);
}
}
}
```
六、配置文件与实体类映射
在application.yml中配置刚才生成的密钥:
```yaml
encrypt:
替换为第三步生成的真实密钥
secret-key: "Xm8s9N/yourGeneratedBase64KeyHere..."
```
接下来,在需要加密的实体类字段上,指定使用我们刚刚创建的CryptoTypeHandler。假设我们有一个ArchiveFile实体,其中content字段(档案正文)需要加密存储:
```java
import com.baomidou.mybatisplus.annotation.TableName;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeReference;
import lombok.Data;
@Data
@TableName("t_archive_file")
public class ArchiveFile {
private Long id;
private String fileName;
// 核心配置:typeHandler 指定全限定类名
// jdbcType = VARCHAR 对应数据库中的字段类型
// 只要查询这个字段,MyBatis 就会自动调用 CryptoTypeHandler 进行解密
@org.apache.ibatis.annotations.JdbcType(JdbcType.VARCHAR)
@org.apache.ibatis.annotations.TypeHandler(CryptoTypeHandler.class)
private String content;
private String creator;
}
```
对应的数据库表结构 SQL(MySQL示例):
```sql
CREATE TABLE t_archive_file (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
file_name VARCHAR(255) NOT NULL,
content TEXT, -- 加密后的密文也是字符串,存储在 TEXT 或 VARCHAR 中
creator VARCHAR(100)
);
```
七、验证与测试
编写一个单元测试或Controller接口来验证效果。注意观察数据库中实际存储的内容。
```java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ArchiveServiceTest {
@Autowired
private ArchiveMapper archiveMapper; // 假设你有一个基础的 Mapper
@Test
public void testEncryption() {
ArchiveFile file = new ArchiveFile();
file.setFileName("绝密档案001.txt");
file.setCreator("admin");
// 业务层操作:直接赋值,不需要手动加密
String originalContent = "这是机密档案内容:身份证号123456789";
file.setContent(originalContent);
// 插入数据库
archiveMapper.insert(file);
System.out.println("插入成功,ID: " + file.getId());
}
@Test
public void testDecryption() {
// 查询数据库
ArchiveFile file = archiveMapper.selectById(1L);
// 输出结果
System.out.println("文件名: " + file.getFileName());
// 这里输出的应该是解密后的明文
System.out.println("档案内容: " + file.getContent());
// 断言验证
assert file.getContent().contains("身份证号123456789");
}
}
```
验证步骤:
- 执行
testEncryption: 程序运行成功后,直接连接数据库,查看t_archive_file表的content字段。你应该看到一串乱码般的Base64字符串(例如:8fJk...==),绝不是明文。
- 执行
testDecryption: 观察控制台输出。程序打印出的file.getContent()应该完整还原为“这是机密档案内容:身份证号123456789”,证明解密逻辑自动生效。
- 篡改测试: 手动修改数据库中
content的一个字符,再次运行查询。程序应抛出解密异常(因为GCM校验Tag失败),证明了数据的防篡改能力。