1 加密工具类
@Slf4j
public class AESUtil {
/**
* 密钥长度: 128, 192 or 256
*/
private static final int KEY_SIZE = 256;
/**
* 加密/解密算法名称
*/
private static final String ALGORITHM = "AES";
/**
* 随机数生成器(RNG)算法名称
*/
private static final String RNG_ALGORITHM = "SHA1PRNG";
/**
* 生成密钥的种子不可泄露
*/
public static final String KEY = "xxxxxxxxxxxx";
/**
* 生成密钥对象
*/
private static SecretKey generateKey(byte[] key) throws Exception {
// 创建安全随机数生成器
SecureRandom random = SecureRandom.getInstance(RNG_ALGORITHM);
// 设置 密钥key的字节数组 作为安全随机数生成器的种子
random.setSeed(key);
// 创建 AES算法生成器
KeyGenerator gen = KeyGenerator.getInstance(ALGORITHM);
// 初始化算法生成器
gen.init(KEY_SIZE, random);
// 生成 AES密钥对象, 也可以直接创建密钥对象: return new SecretKeySpec(key, ALGORITHM);
return gen.generateKey();
}
/**
* 数据加密: 明文 -> 密文
*/
public static String encrypt(String content, byte[] key) {
// 生成密钥对象
SecretKey secKey;
try {
secKey = generateKey(key);
// 获取 AES 密码器
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 初始化密码器(加密模型)
cipher.init(Cipher.ENCRYPT_MODE, secKey);
// 加密数据, 返回密文
byte[] plainBytes = content.getBytes("utf-8");
byte[] result = cipher.doFinal(plainBytes);
return Base64.getEncoder().encodeToString(result);//通过Base64转码返回
} catch (Exception e) {
throw new EncrypErrorException(10000, "AES 加密失败:" + e.getMessage());
}
}
/**
* 数据解密: 密文 -> 明文
*/
public static String decrypt(String content, byte[] key) {
try {
// 生成密钥对象
SecretKey secKey = generateKey(key);
// 获取 AES 密码器
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 初始化密码器(解密模型)
cipher.init(Cipher.DECRYPT_MODE, secKey);
// 解密数据, 返回明文
byte[] result = cipher.doFinal(Base64.getDecoder().decode(content));
return new String(result, "utf-8");
} catch (Exception e) {
throw new EncrypErrorException(10001, "AES 解密失败:" + e.getMessage());
}
}
/**
* 加密文件: 明文输入 -> 密文输出
*/
public static void encryptFile(File plainIn, File cipherOut, byte[] key) throws Exception {
aesFile(plainIn, cipherOut, key, true);
}
/**
* 解密文件: 密文输入 -> 明文输出
*/
public static void decryptFile(File cipherIn, File plainOut, byte[] key) throws Exception {
aesFile(plainOut, cipherIn, key, false);
}
/**
* AES 加密/解密文件
*/
private static void aesFile(File plainFile, File cipherFile, byte[] key, boolean isEncrypt) throws Exception {
// 获取 AES 密码器
Cipher cipher = Cipher.getInstance(ALGORITHM);
// 生成密钥对象
SecretKey secKey = generateKey(key);
// 初始化密码器
cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secKey);
// 加密/解密数据
InputStream in = null;
OutputStream out = null;
try {
if (isEncrypt) {
// 加密: 明文文件为输入, 密文文件为输出
in = new FileInputStream(plainFile);
out = new FileOutputStream(cipherFile);
} else {
// 解密: 密文文件为输入, 明文文件为输出
in = new FileInputStream(cipherFile);
out = new FileOutputStream(plainFile);
}
byte[] buf = new byte[1024];
int len = -1;
// 循环读取数据 加密/解密
while ((len = in.read(buf)) != -1) {
out.write(cipher.update(buf, 0, len));
}
out.write(cipher.doFinal()); // 最后需要收尾
out.flush();
} finally {
close(in);
close(out);
}
}
private static void close(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException e) {
// nothing
}
}
}
}
测试:
public static void main(String[] args) {
String content = "[{\"code\":10101,\"name\":\"基本工资\",\"value\":\"5120.1\"},{\"code\":10102,\"name\":\"岗位工资\",\"value\":\"2000.2\"},{\"code\":10103,\"name\":\"职务工资\",\"value\":\"222.01\"}]";
System.out.println("原文:" + content);
String encrypt = AESUtil.encrypt(content, AESUtil.KEY.getBytes());
System.out.println("密文:" + encrypt + " 长度:" + encrypt.length());
String decrypt = AESUtil.decrypt(encrypt, AESUtil.KEY.getBytes());
System.out.println("解密后原文:" +decrypt);
System.out.println(decrypt.equals(content));
}
结果:
2.mybaties参数,结果处理器
通过使用MyBatis的typeHandler功能,对入参和出参进行处理,实现无缝加密解密(将明文加密后保存至数据库;从数据库读取时,自动将密文解密成明文)
public class AESEncryptHandler extends BaseTypeHandler<Object> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
String content = (parameter == null) ? "" : parameter.toString();
ps.setString(i, AESUtil.encrypt(content, AESUtil.KEY.getBytes()));
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
return AESUtil.decrypt(columnValue, AESUtil.KEY.getBytes());
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException{
String columnValue = rs.getString(columnIndex);
return AESUtil.decrypt(columnValue, AESUtil.KEY.getBytes());
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException{
String columnValue = cs.getString(columnIndex);
return AESUtil.decrypt(columnValue, AESUtil.KEY.getBytes());
}
}
3.实体属性配置:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName(value = "salary_change_record",autoResultMap = true)
@ApiModel(value="HrmSalaryChangeRecord对象", description="定薪调薪记录表")
public class HrmSalaryChangeRecord extends BaseEntity<HrmSalaryChangeRecord> implements Serializable{
private static final long serialVersionUID=1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "员工id")
private Integer employeeId;
@ApiModelProperty(value = "记录类型 1 定薪 2 调薪")
private Integer recordType;
@ApiModelProperty(value = "调薪原因 0 入职定薪 1 入职核定 2 转正 3 晋升 4 调动 5 年中调薪 6 年度调薪 7 特别调薪 8 其他")
private Integer changeReason;
@ApiModelProperty(value = "生效时间")
@DateTimeFormat(pattern="yyyy-MM-dd")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
private Date enableDate;
@ApiModelProperty(value = "试用期调整前工资")
@TableField(typeHandler = AESEncryptHandler.class)
private String proBeforeSum;
@ApiModelProperty(value = "试用期调整后工资")
@TableField(typeHandler = AESEncryptHandler.class)
private String proAfterSum;
@ApiModelProperty(value = "试用期工资明细")
@TableField(typeHandler = AESEncryptHandler.class)
private String proSalary;
@ApiModelProperty(value = "正式调整前工资 json")
@TableField(typeHandler = AESEncryptHandler.class)
private String beforeSum;
@ApiModelProperty(value = "正式调整后工资")
@TableField(typeHandler = AESEncryptHandler.class)
private String afterSum;
@ApiModelProperty(value = "正式工资明细 json")
@TableField(typeHandler = AESEncryptHandler.class)
private String salary;
@ApiModelProperty(value = "状态 0 未生效 1 已生效 2 已取消")
private Integer status;
@ApiModelProperty("员工状态")
private Integer employeeStatus;
@ApiModelProperty(value = "备注")
private String remarks;
@ApiModelProperty(value = "调整前总工资")
@TableField(typeHandler = AESEncryptHandler.class)
private String beforeTotal;
@ApiModelProperty(value = "调整后总工资")
@TableField(typeHandler = AESEncryptHandler.class)
private String afterTotal;
}
要开启autoResultMap 自动配置ResultMap
使用@TableField注解标识属性,并通过typeHandler 指定我们自定义的mybaties 参数结果处理器。
@TableField(typeHandler = AESEncryptHandler.class)
4 demo
4.1 测试mybaties-plus自带方法的新增:
@Test
public void testInsert(){
HrmSalaryChangeRecord salaryChangeRecord=new HrmSalaryChangeRecord();
salaryChangeRecord.setAfterTotal("25343.31").setSalary("{\"newSalary\":[{\"code\":10101,\"name\":\"基本工资\",\"value\":\"5120.1\"},{\"code\":10102,\"name\":\"岗位工资\",\"value\":\"20001.2\"},{\"code\":10103,\"name\":\"职务工资\",\"value\":\"222.01\"}],\"oldSalary\":[{\"code\":10101,\"name\":\"基本工资\",\"value\":\"5120.1\"},{\"code\":10102,\"name\":\"岗位工资\",\"value\":\"2000.2\"},{\"code\":10103,\"name\":\"职务工资\",\"value\":\"222.01\"}]}")
.setBeforeTotal("7342.31").setEmployeeId(2).setRecordType(2).setChangeReason(1).setEnableDate(new Date())
.setProBeforeSum("0").setProSalary("{\"newSalary\":[{\"code\":10101,\"name\":\"基本工资\",\"value\":\"0\"},{\"code\":10102,\"name\":\"岗位工资\",\"value\":\"0\"},{\"code\":10103,\"name\":\"职务工资\",\"value\":\"0\"}],\"oldSalary\":[{\"code\":10101,\"name\":\"基本工资\",\"value\":\"0\"},{\"code\":10102,\"name\":\"岗位工资\",\"value\":\"0\"},{\"code\":10103,\"name\":\"职务工资\",\"value\":\"0\"}]}")
.setProAfterSum("0").setBeforeSum("0")
.setAfterSum("7342.31").setStatus(1).setEmployeeStatus(1).setRemarks("123");
salaryChangeServer.insertRecord(salaryChangeRecord);
}
执行sql:
数据库结果:
可见数据已经加密
4.2测试mybaties-plus自带方法查询:
@Test
public void testList(){
List<HrmSalaryChangeRecord> list = salaryChangeServer.list();
list.forEach(System.out::println);
}
查询结果:
查询结果成功解密。
4.3 xml中自定义sql返回实体类型
xml:
<!--无法解密-->
<select id="selectByEmployeeId2" resultType="HrmSalaryChangeRecord">
select *
from salary_change_record
where employee_id = #{employeeId}
</select>
mapper:
HrmSalaryChangeRecord selectByEmployeeId2(Integer employeeId);
测试
@Test
public void TestSelectByEmployeeId2(){
System.out.println(hrmSalaryChangeMapper.selectByEmployeeId2(2));
}
结果:
查询结果没有解密,在xml中自定义sql返回类型为实体类时无法触发TypeHandle。
4.4 xml中自定义sql返回resultMap 在map中配置typeHandler
xml:
<resultMap id="salaryChangeRecord" type="HrmSalaryChangeRecord">
<id property="id" column="id"/>
<result property="employeeId" column="employee_id"/>
<result property="recordType" column="record_type"/>
<result property="changeReason" column="change_reason"/>
<result property="proBeforeSum" column="pro_before_sum" typeHandler="xxx.AESEncryptHandler"/>
.....省略剩下的map配置
<!--resultMap 中配置typeHandler 可以实现解密-->
<select id="selectByEmployeeId" resultMap="salaryChangeRecord">
select *
from salary_change_record
where employee_id = #{employeeId}
</select>
测试:
@Test
public void TestSelectByEmployeeId(){
System.out.println(hrmSalaryChangeMapper.selectByEmployeeId(2));
}
测试结果:
成功解密
4.5 mybaties-plus 自带Wrappers查询
测试:
@Test
public void TestSelectByEmployeeId3(){
System.out.println(salaryChangeServer.list(Wrappers.lambdaQuery(HrmSalaryChangeRecord.class).eq(HrmSalaryChangeRecord::getEmployeeId,2)
.select(HrmSalaryChangeRecord::getSalary,HrmSalaryChangeRecord::getBeforeSum)));
}
执行结果:
查询结果可以解密
4.5 mybaties-plus 自带方式更新
测试:
@Test
public void testUpdatae()
{
HrmSalaryChangeRecord salaryChangeRecord = new HrmSalaryChangeRecord();
salaryChangeRecord.setBeforeTotal("500");
salaryChangeRecord.setBeforeSum("1024");
salaryChangeRecord.setSalary("1000000000000");
salaryChangeRecord.setId(9);
salaryChangeServer.updateById(salaryChangeRecord);
System.out.println(hrmSalaryChangeMapper.selectByEmployeeId(2));
}
结果:
更新成功。
数据库结果:
数据库也还是密文状态
4.5 mybaties-plus 自带Wrappers方式更新
测试:
@Test
public void testUpdatae2()
{
salaryChangeServer.update(Wrappers.lambdaUpdate(HrmSalaryChangeRecord.class)
.eq(HrmSalaryChangeRecord::getEmployeeId,2).set(HrmSalaryChangeRecord::getSalary,"20000000000")
.set(HrmSalaryChangeRecord::getBeforeSum,"2048"));
}
数据库结果:
数据库成功更新但是是明文,Wrappers更新无法使用typeHandel。
解决办法:
在 set值时使用AESUtil.encrypt加密数据。
@Test
public void testUpdatae3()
{
salaryChangeServer.update(Wrappers.lambdaUpdate(HrmSalaryChangeRecord.class)
.eq(HrmSalaryChangeRecord::getEmployeeId,2).set(HrmSalaryChangeRecord::getSalary, AESUtil.encrypt("500000",AESUtil.KEY.getBytes()))
.set(HrmSalaryChangeRecord::getBeforeSum,AESUtil.encrypt("3000",AESUtil.KEY.getBytes())));
System.out.println(hrmSalaryChangeMapper.selectByEmployeeId(2));
}
执行结果:
数据库数据:
还是密文说明加密成功。
5.结论
5.1新增
1)mybaties-plus自带的新增可以加密
5.2查询:
1)mybaties-plus自带的查询方法和wrappers查询都可以解密
2)xml中自定义sql 返回实体类不能解密,返回resultMap在map中配置typeHandler可以解密。
5.3更新
1)mybaties-plus自带的更新方法可以加密,
2)自带的wrappers更新不能加密,需要将数据加密后更新。