注解方式实现数据库字段加密与解密

目录

前言

一些敏感信息存入数据需要进行加密处理,比如电话号码,身份证号码等,从数据库取出到前端展示时需要解密,如果分别在存入取出时去做处理,会很繁锁,至此,我查了很多相关资料,最后得到一个比较完美的解决方案。

实现步骤

定义注解

1、实体注解@SensitiveEntity

bash 复制代码
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SensitiveEntity {

}

2、字段注解@SensitiveEntity

bash 复制代码
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SensitiveField {

}

加密工具类

AesFieldUtils.java

bash 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


@Slf4j
@Component
public class AesFieldUtils {
    /**
     * 加密算法
     */
    private final String KEY_ALGORITHM = "AES";
    /**
     * 算法/模式/补码方式
     */
    private final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
    /**
     * 编码格式
     */
    private final String CODE = "utf-8";
    /**
     * base64验证规则
     */
    private static final String BASE64_RULE = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)=?$";

    /**
     * 正则验证对象
     */
    private static final Pattern PATTERN = Pattern.compile(BASE64_RULE);
    /**
     * 加解密 密钥key
     */
    @Value("${aes.key}")
    private String key;


    /**
     * @param content 加密字符串
     * @return 加密结果
     */
    public String encrypt(String content) {
        return encrypt(content, key);
    }

    /**
     * 加密
     *
     * @param content 加密参数
     * @param key     加密key
     * @return 结果字符串
     */
    public String encrypt(String content, String key) {
        //判断如果已经是base64加密字符串则返回原字符串
        if (isBase64(content)) {
            return content;
        }
        // 为了安全起见,暂时不加密,需要时再放开
        //return content;
         byte[] encrypted = encrypt2bytes(content, key);
        if (null == encrypted || encrypted.length < 1) {
            log.error("加密字符串[{}]转字节为null", content);
            return null;
        }
        return Base64Utils.encodeToString(encrypted);

    }

    /**
     * @param content 加密字符串
     * @param key     加密key
     * @return 返回加密字节
     */
    public byte[] encrypt2bytes(String content, String key) {
        try {
            byte[] raw = key.getBytes(CODE);
            SecretKeySpec secretKeySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
            return cipher.doFinal(content.getBytes(CODE));
        } catch (Exception e) {
            log.error("failed to encrypt: {} of {}", content, e);
            return null;
        }
    }

    /**
     * @param content 加密字符串
     * @return 返回加密结果
     */
    public String decrypt(String content) {
        try {
            return decrypt(content, key);
        } catch (Exception e) {
            log.error("failed to decrypt: {}, e: {}", content, e);
            return null;
        }
    }

    /**
     * 解密
     *
     * @param content 解密字符串
     * @param key     解密key
     * @return 解密结果
     */
    public String decrypt(String content, String key) throws Exception {
        //不是base64格式字符串则不进行解密
        if (!isBase64(content)) {
            return content;
        }
        // 为了安全起见,暂时不加密解密,需要时再放开
        //return content;
        return decrypt(Base64Utils.decodeFromString(content), key);
    }

    /**
     * @param content 解密字节
     * @param key     解密key
     * @return 返回解密内容
     */
    public String decrypt(byte[] content, String key) throws Exception {
        if (key == null) {
            log.error("AES key should not be null");
            return null;
        }

        byte[] raw = key.getBytes(CODE);
        SecretKeySpec keySpec = new SecretKeySpec(raw, KEY_ALGORITHM);
        Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        try {
            byte[] original = cipher.doFinal(content);
            return new String(original, CODE);
        } catch (Exception e) {
            log.error("failed to decrypt content: {}/ key: {}, e: {}", content, key, e);
            return null;
        }
    }

    /**
     * 判断是否为 base64加密
     *
     * @param str 参数
     * @return 结果
     */
    public static boolean isBase64(String str) {
        Matcher matcher = PATTERN.matcher(str);
        return matcher.matches();
    }

}

其中aes.key为加密key随便填

定义mybatis拦截器

MyBatisInterceptor.java

bash 复制代码
import cn.hutool.core.util.ReflectUtil;
import com.hw.common.annotation.SensitiveEntity;
import com.hw.common.service.AesService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;

@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
public class MyBatisInterceptor implements Interceptor {
    @Resource
    private AesService aesService;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        //拦截sql结果处理器
        if (target instanceof ResultSetHandler) {
            return resultDecrypt(invocation);
        }
        //拦截sql参数处理器
        if (target instanceof ParameterHandler) {
            return parameterEncrypt(invocation);
        }
        //拦截sql语句处理器
        if (target instanceof StatementHandler) {
            return replaceSql(invocation);
        }
        return invocation.proceed();
    }

    /**
     * 对mybatis映射结果进行字段解密
     *
     * @param invocation 参数
     * @return 结果
     * @throws Throwable 异常
     */
    private Object resultDecrypt(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            ArrayList resultList = (ArrayList) resultObject;
            if (CollectionUtils.isEmpty(resultList)) {
                return resultObject;
            }
            for (Object result : resultList) {
                if (needToDecrypt(result)) {
                    //逐一解密
                    aesService.decrypt(result);
                }
            }
            //基于selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                aesService.decrypt(resultObject);
            }
        }
        return resultObject;
    }

    /**
     * mybatis映射参数进行加密
     *
     * @param invocation 参数
     * @return 结果
     * @throws Throwable 异常
     */
    private Object parameterEncrypt(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        //取出实例
        Object parameterObject = parameterField.get(parameterHandler);

        if(parameterHandler.getParameterObject() instanceof MapperMethod.ParamMap){
            MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterHandler.getParameterObject();
            parameterObject = paramMap.get("param1");
        }
        if (null == parameterObject) {
            return invocation.proceed();
        }
        Class<?> parameterObjectClass = parameterObject.getClass();
        //校验该实例的类是否被@SensitiveEntity所注解
        SensitiveEntity sensitiveEntity = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveEntity.class);
        //未被@SensitiveEntity所注解 则为null
        if (Objects.isNull(sensitiveEntity)) {
            return invocation.proceed();
        }
        //取出当前当前类所有字段,传入加密方法
        //Field[] declaredFields = parameterObjectClass.getDeclaredFields();
        Field[] allFields = ReflectUtil.getFields(parameterObjectClass);
        aesService.encrypt(allFields, parameterObject);
        return invocation.proceed();
    }

    /**
     * 替换mybatis Sql中的加密Key
     *
     * @param invocation 参数
     * @return 结果
     * @throws Throwable 异常
     */
    private Object replaceSql(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        //获取到原始sql语句
        String sql = boundSql.getSql();
        if (null == sql){
            return invocation.proceed();
        }
        //通过反射修改sql语句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, sql);
        return invocation.proceed();
    }

    /**
     * 判断是否包含需要加解密对象
     *
     * @param object 参数
     * @return 结果
     */
    private boolean needToDecrypt(Object object) {
        if(Objects.isNull(object)){
            return false;
        }
        Class<?> objectClass = object.getClass();
        Class<?> parentClass = objectClass.getSuperclass();
        SensitiveEntity sensitiveEntity = AnnotationUtils.findAnnotation(objectClass, SensitiveEntity.class);
        SensitiveEntity parentSensitiveEntity = AnnotationUtils.findAnnotation(parentClass, SensitiveEntity.class);

        return Objects.nonNull(sensitiveEntity) || Objects.nonNull(parentSensitiveEntity);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

}

总结

mybatis拦截器非常强大,它可以对所有存入数据库的数据进行处理,包括更新,插入,查询和删除...,对数据做统一处理非常方便。

相关推荐
tatasix9 分钟前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。21 分钟前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了23 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度24 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮27 分钟前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9991 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98762 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发
苏-言2 小时前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
Ljw...2 小时前
索引(MySQL)
数据库·mysql·索引