一、前言
客户要求系统的敏感字段需要使用国密SM4算法进行加密,需要在数据库中看到加密的数据。因为平台的持久层使用的是Hibernate,因此利用hibernate的拦截器在数据读取时进行解密,在数据进行持久化时进行加密实现。
二、实现思路
1、敏感实体类上添加加密注解,可以通过注解区分出哪些实体类需要加密
2、敏感实体类的字段也需要增加加密注解,用于区分哪些字段需要加密
3、利用在Hibernate的拦截器EmptyInterceptor的对应事件,通过反射获取需要处理的实体类和字段,在数据入库前在拦截器对应的方法对数据进行加密处理,在数据读取时在拦截器中对数据进行解密处理。
4、以上处理当存储之后加密字段已修改成加密,需要重新解密,则利用Hibernate的监听器PostInsertEventListener的对应事件,通过反射获取需要处理的实体类和字段,在存储成功后在进行解密(若保存后无需使用该对象时可不写监听器)。
三、Hibernate拦截器简介
Hibernate定义了一个拦截器,位于org.hibernate.Interceptor,提供了一系列的拦截器方法。详细可见Hibernate官网文档
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;
}
public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
return false;
}
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
return false;
}
public void postFlush(Iterator entities) {
}
public void preFlush(Iterator entities) {
}
public Boolean isTransient(Object entity) {
return null;
}
public Object instantiate(String entityName, EntityMode entityMode, Serializable id) {
return null;
}
public int[] findDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
return null;
}
...
}
这里只需要用到三个方法,分别是onLoad初始化前调用、onSave保存前调用和onFlushDirty更新对象前调用。需要注意的是onSave的方法并不是指保存时调用,而是指Hibernate执行insert操作时才会调用,而update操作对应的拦截方法是onFlushDirty
方法名 | 描述 |
onLoad | 在初始化对象之前调用。拦截器可能会更改状态,该状态将被传播到持久对象。请注意,当调用此方法时,实体将是该类的一个未初始化的空实例。 |
onSave | 在保存对象之前调用。拦截器可以修改状态,该状态将用于SQL插入并传播到持久对象。 |
onFlushDirty | 在冲洗过程中检测到对象变脏时调用。拦截器可以修改检测到的currentState,它将被传播到数据库和持久对象。请注意,并非所有刷新都以与数据库的实际同步结束,在这种情况下,新的currentState将传播到对象,但不一定(立即)传播到数据库。强烈建议拦截器不要修改以前的状态。 |
Hibernate监听器简介
hibernate的监听器定义,不需要太复杂的代码,不需要继承或实现某个接口,仅通过注解就能实现,如前面使用的@PrePersist,@PreUpdate,@PostLoad等。常用注解列举如下:
注解 | 使用 |
@PrePersist | 在插入之前调用 |
@PostPersist | 在插入之后调用 |
@PerUpdate | 在更新之前调用 |
@PostUpdate | 在更新之后调用 |
@PerRemove | 在删除之前调用 |
@PostRemove | 在删除之后调用 |
@PostLoad | 在查询之后调用(转换对象之前) |
实现方式
自定义加解密标记注解
标记实体类是否需要进行加解密注解
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 {
}
将注解加到实体类上
@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] =xxx(state[i].toString());
LOGGER.info("当前字段:{}, 加密后:{}", aField, state[i]);
} else if ("onLoad".equals(type)) {
LOGGER.info("当前字段:{}, 解密前:{}", aField, state[i]);
//调用解密方法
state[i] = xxx(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;
}
}
实现监听器
import com.chis.common.utils.CacheUtils;
import com.chis.common.utils.StringUtils;
import com.chis.common.utils.sm.Sm4Util;
import com.chis.modules.system.encrypt.annotation.EncryptField;
import com.chis.modules.system.encrypt.annotation.EncryptTable;
import org.hibernate.event.PostInsertEvent;
import org.hibernate.event.PostInsertEventListener;
import org.hibernate.event.def.DefaultLoadEventListener;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* <p>类描述:hibernate保存之后调用 监听器</p>
* @ClassAuthor 2023-01-14 8:52
*/
public class EncryptListener extends DefaultLoadEventListener implements PostInsertEventListener {
@Override
public void onPostInsert(PostInsertEvent postInsertEvent) {
try {
//是否需要加解密
String infoEncryKey = CacheUtils.get("ENCRYKEY","infoEncryKey") == null ? null : CacheUtils.get("ENCRYKEY","infoEncryKey").toString();
if(StringUtils.isBlank(infoEncryKey)){
return;
}
Object entity = postInsertEvent.getEntity();
List<String> annotationFields = getAnnotationField(entity);
if(CollectionUtils.isEmpty(annotationFields)){
return ;
}
for (String aField : annotationFields) {
Class<?> cla = entity.getClass();
Field[] fields = cla.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String keyName = field.getName();
Object value = field.get(entity);
if(keyName.equals(aField)){
//解密
field.set(entity,xxx(value.toString(),infoEncryKey));
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
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 (null == encryptField) {
continue;
}
fields.add(field.getName());
}
return fields;
}
}
还需要在persistence.xml配置文件中添加配置(不同的框架配置不一样)
<property name="hibernate.ejb.interceptor"
value="xxx.EncryptInterceptor" />
<property name="hibernate.ejb.event.post-insert"
value="xxx.EncryptListener" />
sql加解密通用util
import com.chis.common.utils.CacheUtils;
import com.chis.common.utils.StringUtils;
import com.chis.common.utils.sm.Sm4Util;
/**
* <p>类描述: 加解密字段单独处理-只适合精确查询</p>
* @ClassAuthor 2023-01-14 9:10
*/
public class EncryptFieldUtil {
/**
* <p>方法描述:加密</p>
* @MethodAuthor 2023-01-14 9:13
*/
public static String strEncode(String str) {
String infoEncryKey = "";
if(StringUtils.isNotBlank(infoEncryKey)){
return xxx(str,infoEncryKey);
}
return str;
}
/**
* <p>方法描述:解密</p>
* @MethodAuthor 2023-01-14 9:13
*/
public static String strDecode(String str) {
String infoEncryKey = "";
if(StringUtils.isNotBlank(infoEncryKey)){
return xxx(str,infoEncryKey);
}
return str;
}
}
小结
这种思路其实不太具有通用性,特别是如果代码中有使用原生sql方式的话,处理会比较麻烦,但如果只是个别实体类的敏感字段需要加密解密处理的话,是比较方便的处理方式。
如果您能够使用JPA2.1,我建议在AttributeConverter实现中使用@Convert注释。
AttributeConverter定义了实体属性在序列化到数据存储时和从数据存储反序列化时的状态之间的协定。