目录
- 前言
- 实现思路
- Hibernate拦截器介绍
- 实现方式
- 自定义加解密标记注解
- 实现拦截器
- 加解密工具类
- 小结
前言
前段时间刚好在公司处理到这种需求,客户要求系统的敏感字段需要使用国密SM4算法进行加密,需要在数据库中看到加密的数据。因为公司的持久层使用的是Hibernate,因此利用hibernate的拦截器在数据读取时进行解密,在数据进行持久化时进行加密实现。
实现思路
1、敏感实体类上添加加密注解,可以通过注解区分出哪些实体类需要加密
2、敏感实体类的字段也需要增加加密注解,用于区分哪些字段需要加密
3、利用在Hibernate的拦截器EmptyInterceptor
的对应事件,通过反射获取需要处理的实体类和字段,在数据入库前在拦截器对应的方法对数据进行加密处理,在数据读取时在拦截器中对数据进行解密处理。
Hibernate拦截器简介
Hibernate定义了一个拦截器,位于org.hibernate.Interceptor
,提供了一系列的拦截器方法。详细可见Hibernate官网文档
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.hibernate;
import java.io.Serializable;
import java.util.Iterator;
import org.hibernate.type.Type;
public interface Interceptor {
// 加载数据库时调用
boolean onLoad(Object var1, Serializable var2, Object[] var3, String[] var4, Type[] var5) throws CallbackException;
// 更新操作时调用
boolean onFlushDirty(Object var1, Serializable var2, Object[] var3, Object[] var4, String[] var5, Type[] var6) throws CallbackException;
// 添加操作时调用
boolean onSave(Object var1, Serializable var2, Object[] var3, String[] var4, Type[] var5) throws CallbackException;
// 其他方法省略...
}
但如果直接实现Interceptor
,我们还需要实现该接口下面的所有方法,为此Hibernate为我们提供了空拦截器EmptyInterceptor
。EmptyInterceptor空拦截器继承自Interceptor拦截器,已经帮我们实现接口内所有的方法,这样就不需要实现所有接口方法了。我们可以定义一个类去继承空拦截器,根据需要去重写空拦截器里面提供的方法。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.hibernate;
import java.io.Serializable;
import java.util.Iterator;
import org.hibernate.type.Type;
public class EmptyInterceptor implements Interceptor, Serializable {
public static final Interceptor INSTANCE = new EmptyInterceptor();
protected EmptyInterceptor() {
}
public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
}
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
return false;
}
// 其他方法省略...
}
这里只需要用到三个方法,分别是onLoad
初始化前调用、onSave
保存前调用和onFlushDirty
更新对象前调用。需要注意的是onSave
的方法并不是指保存时调用,而是指Hibernate执行insert操作时才会调用,而update操作对应的拦截方法是onFlushDirty,可以在官网文档中查到onSave
方法描述:
Called before an object is saved. The interceptor may modify the
state
, which will be used for the SQLINSERT
and propagated to the persistent object.
方法名 | 描述 |
---|---|
onLoad | 在初始化对象之前调用。拦截器可能会更改状态,该状态将被传播到持久对象。请注意,当调用此方法时,实体将是该类的一个未初始化的空实例。 |
onSave | 在保存对象之前调用。拦截器可以修改状态,该状态将用于SQL插入并传播到持久对象。 |
onFlushDirty | 在冲洗过程中检测到对象变脏时调用。拦截器可以修改检测到的currentState,它将被传播到数据库和持久对象。请注意,并非所有刷新都以与数据库的实际同步结束,在这种情况下,新的currentState将传播到对象,但不一定(立即)传播到数据库。强烈建议拦截器不要修改以前的状态。 |
实现方式
自定义加解密标记注解
标记实体类是否需要进行加解密注解
import java.lang.annotation.*;
/**
* 需要加解密的表注解,只有添加此注解的表才需要进行加解密
*/
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTable {
}
标记实体类中的字段是否需要进行加解密处理注解
import java.lang.annotation.*;
/**
* 加解密表字段,只有添加了此注解的实体类字段才要进行加解密
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptField {
}
将注解加到实体类上
import com.choy.demo.encrypt.annotation.EncryptField;
import com.choy.demo.encrypt.annotation.EncryptTable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 用户信息实体类
*/
@Data
@Entity
@Table(name = UserInfo.TABLE_NAME)
@EncryptTable
public class UserInfo {
public static final String TABLE_NAME = "user_info";
/**
* 主键
*/
@Id
@Column(name = "RID")
private String rid;
/**
* 用户名
*/
@EncryptField
@Column(name = "user_name")
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
@EncryptField
@Column(name = "NICKNAME")
private String nickname;
/**
* 学历
*/
@EncryptField
private String education;
}
实现拦截器
拦截器代码块
import com.choy.demo.encrypt.annotation.EncryptField;
import com.choy.demo.encrypt.annotation.EncryptTable;
import com.choy.demo.utils.RSAEncryptUtils;
import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* hibernate加解密拦截器
*/
@Component
public class EncryptInterceptor extends EmptyInterceptor {
private final static Logger LOGGER = LoggerFactory.getLogger(EncryptInterceptor.class);
/**
* 更新时调用
*
* @param entity 实体类
* @param id 主键
* @param currentState 当前实体类对应的值
* @param previousState 修改前实体类对应的值
* @param propertyNames 字段名
* @param types 实体类每个属性类型对应hibernate的类型
* @return true | false true才会修改数据
*/
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
Object[] newState = dealField(entity, currentState, propertyNames, "onFlushDirty");
return super.onFlushDirty(entity, id, newState, previousState, propertyNames, types);
}
/**
* 加载时调用
*
* @param entity 实体类
* @param id 主键
* @param state 实体类对应的值
* @param propertyNames 字段名
* @param types 实体类每个属性类型对应hibernate的类型
* @return true | false true才会修改数据
*/
@Override
public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
Object[] newState = dealField(entity, state, propertyNames, "onLoad");
return super.onLoad(entity, id, newState, propertyNames, types);
}
/**
* 保存时调用
*
* @param entity 实体类
* @param id 主键
* @param state 实体类对应的值
* @param propertyNames 字段名
* @param types 实体类每个属性类型对应hibernate的类型
* @return true | false true才会修改数据
*/
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
Object[] newState = dealField(entity, state, propertyNames, "onSave");
return super.onSave(entity, id, newState, propertyNames, types);
}
/**
* 处理字段对应的数据
*
* @param entity 实体类
* @param state 数据
* @param propertyNames 字段名称
* @return 解密后的字段名称
*/
private Object[] dealField(Object entity, Object[] state, String[] propertyNames, String type) {
List<String> annotationFields = getAnnotationField(entity);
LOGGER.info("调用方法:{}, 需要加密的字段:{}", type, annotationFields);
// 遍历字段名和加解密字段名
for (String aField : annotationFields) {
for (int i = 0; i < propertyNames.length; i++) {
if (!propertyNames[i].equals(aField)) {
continue;
}
// 如果字段名和加解密字段名对应且不为null或空
if (state[i] == null || Objects.equals(state[i].toString(), "")) {
continue;
}
if ("onSave".equals(type) || "onFlushDirty".equals(type)) {
LOGGER.info("当前字段:{}, 加密前:{}", aField, state[i]);
state[i] = RSAEncryptUtils.encrypt(state[i].toString());
LOGGER.info("当前字段:{}, 加密后:{}", aField, state[i]);
} else if ("onLoad".equals(type)) {
LOGGER.info("当前字段:{}, 解密前:{}", aField, state[i]);
state[i] = RSAEncryptUtils.decrypt(state[i].toString());
LOGGER.info("当前字段:{}, 解密后:{}", aField, state[i]);
}
}
}
return state;
}
/**
* 获取实体类中带有注解EncryptField的变量名
*
* @param entity 实体类
* @return 需要加解密的字段
*/
private List<String> getAnnotationField(Object entity) {
// 判断当前实体类是否有加解密注解
Class<?> entityClass = entity.getClass();
if (!entityClass.isAnnotationPresent(EncryptTable.class)) {
return Collections.emptyList();
}
List<String> fields = new ArrayList<>();
// 获取实体类下的所有成员并判断是否存在加解密注解
Field[] declaredFields = entityClass.getDeclaredFields();
for (Field field : declaredFields) {
EncryptField encryptField = field.getAnnotation(EncryptField.class);
if (Objects.isNull(encryptField)) {
continue;
}
fields.add(field.getName());
}
return fields;
}
}
加解密工具类
因为国密sm4是需要引用其他jar,还需要使用到密码机,所以这里用了RSA非对称加密替换实现。
import org.apache.tomcat.util.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA加密工具
*/
public class RSAEncryptUtils {
private static final Logger logger = LoggerFactory.getLogger(RSAEncryptUtils.class);
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文长度
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* 公钥
*/
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCogucUuwHtWibL00LTue" +
"IL2e8DSjNb0TsZebxa4V45hVzukV8L/74a2BXHwEcfy7mpdGmPm9pIt0nFOqoxAM1Y6cO3LAZ2eVPjLGAlwsKCZ3pAv" +
"Uwi0LVpEqpYwATVAnIIpWwsMjhfFDJ1NkjGY7IMWVnM9VPQ/paq/0XiVEYSOQIDAQAB";
/**
* 私钥
*/
private static final String PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKiC5xS7A" +
"e1aJsvTQtO54gvZ7wNKM1vROxl5vFrhXjmFXO6RXwv/vhrYFcfARx/Lual0aY+b2ki3ScU6qjEAzVjpw7csBnZ5U+Ms" +
"YCXCwoJnekC9TCLQtWkSqljABNUCcgilbCwyOF8UMnU2SMZjsgxZWcz1U9D+lqr/ReJURhI5AgMBAAECgYBQnwhl367" +
"lWxtyqymu2KEwoFz9CvQVer42ywp1xJtrE8ZJkZ2SxRG0ECwjfHfK25KBY2PZxGwkHCUcSpwAg+y6VLhUla5giez+WQ" +
"Eu5iSNCKgeZlbRqvvUQ/9OurujF3+nBdJm288LfcQSTHBBRRlTkAjRAhGIDVfDygJqUSuAvQJBAOTRw+3LhI2ZrcioT" +
"156LnmUAKUj0RLUbMqXYt8nhGhhEsTsD0cwfQKTHg0pS7oyyPDfvYvKT/TPfmA3kXbtGycCQQC8hzZY+km6bvx1QFNI" +
"TpremQeI4C9vkYSIgybqGHwiWe5clxlMqdlskjQCDQ3ZmyXoFfycNc7fPfvnuiDcQUOfAkBU8KlStKHYDpw8SH5uC90" +
"EtLQomUsbOk/IRLonLHwyYxackyR4wL8nHYWiTRoXXJLLF8M9CTT1I7E99mLBSvMxAkBlgY+bfLcxsAwxvT6aEeiErX" +
"RHGB2yPnFTZvoO1LwRasZSB/DRPCoasOVbrVelsElKmnv2R2po/GCjNa33qRQVAkEAgvkmnTCO8HwOUQagCksl/PlEz" +
"Hpbxb/lkgcr6xyP/N/QbwB45UKr0MrAYg0UdPai7Y3NqbowQXQ0tgwnGsUMdw==";
/**
* 字符串公钥分段加密
*
* @param str 要加密的字符串
* @return 加密后的字符串
*/
public static String encrypt(String str) {
byte[] result = null;
try {
// base64解码的公钥
byte[] decoded = Base64.decodeBase64(PUBLIC_KEY);
// 初始化公钥
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
// 初始化Cipher
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
byte[] data = str.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 待加密数据长度
int dataLength = data.length;
// offset为已经处理的长度,每次循环后,offset需要加上117
for (int offset = 0, i = 0; dataLength - offset > 0; offset = MAX_ENCRYPT_BLOCK * ++i) {
byte[] cache = null;
// 当加密长度大于117时
if (dataLength - offset > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offset, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offset, dataLength - offset);
}
byteArrayOutputStream.write(cache);
}
result = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
} catch (Exception exception) {
logger.info("加密失败: {}", exception.getMessage());
return "";
}
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param str 要解密的字符串
* @return 解密后的字符串
*/
public static String decrypt(String str) {
byte[] result = null;
try {
// base64解码的私钥
byte[] decoded = Base64.decodeBase64(PRIVATE_KEY);
// 初始化私钥
Key privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
// 初始化Cipher
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] data = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 待加密数据长度
int dataLength = data.length;
// offset为已经处理的长度,每次循环后,offset需要加上128
for (int offset = 0, i = 0; dataLength - offset > 0; offset = MAX_DECRYPT_BLOCK * ++i) {
byte[] cache = null;
// 当加密长度大于128时
if (dataLength - offset > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(data, offset, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offset, dataLength - offset);
}
byteArrayOutputStream.write(cache);
}
result = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
} catch (Exception exception) {
logger.info("解密失败: {}", exception.getMessage());
return "";
}
return new String(result);
}
public static void main(String[] args) throws Exception {
// KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// keyPairGenerator.initialize(1024);
// KeyPair keyPair = keyPairGenerator.generateKeyPair();
// PrivateKey aPrivate = keyPair.getPrivate();
// PublicKey aPublic = keyPair.getPublic();
// System.out.println("公钥:" + Base64.encodeBase64String(aPublic.getEncoded()));
// System.out.println("私钥:" + Base64.encodeBase64String(aPrivate.getEncoded()));
String encrypt = RSAEncryptUtils.encrypt("aaaaa");
System.out.println("加密后:" + encrypt);
System.out.println("解密后:" + RSAEncryptUtils.decrypt(encrypt));
}
}
还需要在配置文件中添加配置,引用hibernate的拦截器
# 配置hibernate拦截器
jpa:
properties:
hibernate:
session_factory:
interceptor: com.choy.demo.encrypt.interceptor.EncryptInterceptor # 自定义拦截器的包路径
其他dao、service和controller只是简单实现了查询和保存接口,省略部分代码
// controller层代码
@RestController
@RequestMapping("/user")
public class UserInfoController {
private final IUserInfoService userInfoService;
public UserInfoController(IUserInfoService userInfoService) {
this.userInfoService = userInfoService;
}
/**
* 获取所有用户信息
*
* @return List<UserInfo>
*/
@GetMapping("/list")
public List<UserInfo> listUserInfo(){
return userInfoService.listUserInfo();
}
/**
* 保存用户信息,添加、更新操作
*
* @param userInfo 用户信息实体类
*/
@PostMapping("/save")
public void saveUserInfo(UserInfo userInfo){
if (userInfo.getRid() == null){
userInfo.setRid(UUID.randomUUID().toString());
}
userInfoService.saveUserInfo(userInfo);
}
}
// service接口层代码
public interface IUserInfoService {
/**
* 获取所有用户信息
*
* @return List<UserInfo>
*/
List<UserInfo> listUserInfo();
/**
* 保存用户信息,添加、更新操作
*
* @param userInfo 用户信息实体类
*/
void saveUserInfo(UserInfo userInfo);
}
// service层代码
@Service
public class UserInfoService implements IUserInfoService {
private final IUserInfoDao userInfoDao;
public UserInfoService(IUserInfoDao userInfoDao) {
this.userInfoDao = userInfoDao;
}
/**
* 获取所有用户信息
*
* @return List<UserInfo>
*/
@Override
public List<UserInfo> listUserInfo() {
return userInfoDao.findAll();
}
/**
* 保存用户信息,添加、更新操作
*
* @param userInfo 用户信息实体类
*/
@Override
public void saveUserInfo(UserInfo userInfo) {
userInfoDao.save(userInfo);
}
}
// dao层代码
@Repository(IUserInfoDao.DAO_BEAN_NAME)
public interface IUserInfoDao extends JpaRepository<UserInfo, String>, JpaSpecificationExecutor<UserInfo> {
String DAO_BEAN_NAME = "userInfoDao";
}
实现效果
- 新增用户时,需要加密的字段会加密后再保存到数据库中
- 指定rid时会修改用户信息,因为重写了onFlushDirty方法,所以数据库对应的字段会被重新加密
- 获取用户数据时,会先从数据库中获取数据,经过解密后再返回
小结
这种思路其实不太具有通用性,特别是如果代码中有使用原生sql方式的话,处理会比较麻烦,但如果只是个别实体类的敏感字段需要加密解密处理的话,是比较方便的处理方式。
还有另一种实现方式,可以用Hibernate的监听器,监听各种事件来处理数据。另外再附上本文的源码地址。