大大大大师兄

V1

2022/05/06阅读:30主题:绿意

hibernate拦截器加解密字段

Hibernate拦截器字段加密解密

目录

  • 前言
  • 实现思路
  • 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 InterceptorSerializable {
    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;

    /**
     * 学历
     */

    @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<UserInfoString>, JpaSpecificationExecutor<UserInfo{

    String DAO_BEAN_NAME = "userInfoDao";
}

实现效果

  • 新增用户时,需要加密的字段会加密后再保存到数据库中
image-20220505231411490
image-20220505231411490
image-20220505231423612
image-20220505231423612
  • 指定rid时会修改用户信息,因为重写了onFlushDirty方法,所以数据库对应的字段会被重新加密
image-20220505231546126
image-20220505231546126
image-20220505231615994
image-20220505231615994
  • 获取用户数据时,会先从数据库中获取数据,经过解密后再返回
image-20220505231710197
image-20220505231710197
image-20220505231734360
image-20220505231734360

小结

这种思路其实不太具有通用性,特别是如果代码中有使用原生sql方式的话,处理会比较麻烦,但如果只是个别实体类的敏感字段需要加密解密处理的话,是比较方便的处理方式。

还有另一种实现方式,可以用Hibernate的监听器,监听各种事件来处理数据。另外再附上本文的源码地址

分类:

后端

标签:

后端

作者介绍

大大大大师兄
V1