目录

  • 前言
  • 实现思路
  • 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 SQL INSERT 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;/*** 学历*/@EncryptFieldprivate 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才会修改数据*/@Overridepublic 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才会修改数据*/@Overridepublic 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才会修改数据*/@Overridepublic 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));// 初始化CipherCipher 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需要加上117for (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));// 初始化CipherCipher 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需要加上128for (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>*/@Overridepublic List<UserInfo> listUserInfo() {return userInfoDao.findAll();}/*** 保存用户信息,添加、更新操作** @param userInfo 用户信息实体类*/@Overridepublic 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的监听器,监听各种事件来处理数据。另外再附上本文的源码地址。

Hibernate拦截器字段加密解密相关推荐

  1. Hibernate 拦截器 Hibernate 监听器

    Hibernate拦截器(Interceptor)与事件监听器(Listener) 拦截器(Intercept):与Struts2的拦截器机制基本一样,都是一个操作穿过一层层拦截器,每穿过一个拦截器就 ...

  2. 根据hibernate拦截器实现可配置日志的记录

    对于日志和事件的记录在每个项目中都会用到,如果在每个manager层中触发时间记录的话,会比较难以扩展和维护,所以可配置的日志和事件记录在项目中会用到! 首先在spring的配置文件中加入hibern ...

  3. Hibernate 拦截器实例

    Hibernate 在操作数据库的时候要执行很多操作,这些动作对用户是透明的.这些操作主要是有拦截器和时间组成 hibernate拦截器可以拦截大多数动作,比如事务开始之后(afterTransact ...

  4. Retrofit 在拦截器中加密url 并修改body 参数key-value

    Retrofit 在拦截器中加密url 并修改body 参数key-value 从一个蛋疼的需求说起: URI加密:jjj/ 后面的URI采用AES-CBC-pkcs5padding加密后再base6 ...

  5. 使用mybatis拦截器实现字段加密解密

    前言 .项目中我们存储一些用户信息的使用后根据规定,不可以存储明文,尤其是密码,实现的办法有好多种,今天承接上一篇文章mybatis拦截器,利用拦截器实现使用注解的方式在数据插入前进行加密,查询是自动 ...

  6. Spring-Web - 数据库 字段加密 解密

      在工作中,为了保证数据安全,需要对数据库字段进行加解密,之前工作中就遇到了这种情况,因为线上数据库有很多的人都有权限,运维,账务,运营(通过后台系统查看),出口太多了,但有用户向我们平台举报,说有 ...

  7. Mybatis拦截器安全加解密MySQL数据实战

    需求背景 公司为了通过一些金融安全指标(政策问题)和防止数据泄漏,需要对用户敏感数据进行加密,所以在公司项目中所有存储了用户信息的数据库都需要进行数据加密改造.包括Mysql.redis.mongod ...

  8. java jpa字段加密解密

    公司有个需求,人员身份证号码入库时需要加密,取出时需要解密.由于系统中没有设计加密解密方式,所以需业务中单独处理. 之前考虑加拦截器,后来发现需求不会很大,这种方式太复杂没有必要. 首先添加一个类,处 ...

  9. Hibernate 拦截器的使用--动态表名

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: jpa有多种实现的方式,但是最常见的还是采用Hibernate的方式实现,所以为了实现上述的业务,就必须得用到Hibe ...

  10. oracle实现sha加密解密,oracle部分字段加密解密 实现模糊搜索

    数据库部分字段加密 实现该字段模糊查询 解决方案:从数据库层面 对改字段进行解密 是目前最为方便的 而oracle可以支持将java类带入到oracle从而调用对应的方法. 基本步骤: 编写好对应的加 ...

最新文章

  1. DotNetNuke(DNN)升级攻略(DNN 4.3.7至DNN 4.6.0)
  2. ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
  3. php简单的mysql类_一个简单的php mysql操作类
  4. 平均每个员工2000万!苹果为啥买下这家刚成立3年的AI创业公司?
  5. 长宽相等的矩阵(二维数组)逆时针旋转90度
  6. java 百度副文本_spring boot 、springMVC环境集成百度ueditor富文本编辑器
  7. Flex 与.net 进行通信可以通过Fluorine(fluorinefx),WebORB For .net,Socket
  8. 详解由VS 2010生成的Bug报告(2) - 报告的内容
  9. K60(Cortex-M4)开源开发探索(一)—— K60简介
  10. 集团企业信息化规划和实施研究
  11. 圆通问题频发背后的“罪与罚”
  12. R语言_缺失值NA的处理
  13. Linux-ubuntu系统查看显卡型号、显卡信息详解、显卡天梯图
  14. 郭德纲的网络效应和网络利用
  15. pandas中关于DataFrame去掉重复行和NaN行
  16. 基于SpringBoot架构的心理健康测试系统(免费获取源码+项目介绍+运行介绍+运行截图+论文)
  17. 【C/C++ 经典小程序(一)】
  18. 1625 夹克爷发红包 51HOD
  19. 计算机视觉六大技术:图像分类、目标检测、目标跟踪、语义分割、实例分割、影像重建..
  20. 【解决方案】Microsoft Visual C++ 14.0 is required

热门文章

  1. VS下更新Qt语言家ts文件没反应
  2. mysql笛卡尔积效率_SQL优化 MySQL版 -分析explain SQL执行计划与笛卡尔积
  3. 北斗GNSS无人巡检车辆的高精度定位定向应用方案
  4. 查询mysql数据库的端口号_查询数据库端口号的命令
  5. WPS启动不再默认展示“稻壳”页面 - 去除稻壳的方法
  6. spring boot快速启动(七)——boot与定时任务
  7. php网上阅卷源码,翰林金榜网上阅卷
  8. MATLAB图像分割系统设计
  9. 软件工程实验报告三--需求分析及文档编写
  10. 16进制发送 mqtt客户端调试工具_MQTT测试工具下载