引入

问题

在项目中需要对用户敏感数据进行脱敏处理,例如身份证号、手机号等信息进行加密再入库。

解决思路

方法一:最简单直接的方式就是对代码中所有涉及到敏感数据的接口在查询和插入时进行加解密 

方法二:方法一会对代码入侵很大,需要考虑到所有涉及到的接口,工作量极大,并且可能出现组员协作时没有考虑到对数据加解密的问题。最后决定采用mybatis的插件在mybatis SQL执行和查询结果填充操作上进行切入。上层业务调用不再需要考虑数据的加解密问题同时也保证了数据的脱敏

Mybatis插件原理

Mybatis的插件是通过拦截器实现的,Mabatis支持对四种对象进行拦截

实现

  • 设置参数时对参数中含有敏感字段的数据进行加密
  • 对查询返回的结果进行解密处理

基于上面两种要求,我们只需要对ParameterHandler和ResultSetHandler进行切入。

定义特定注解,在切入时只需要检查字段中是否包含该注解来决定是否加解密

加解密注解

定义SensitiveData注解

import java.lang.annotation.*;
/**
 * 该注解定义在类上
 * 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解
 * 这个注解要配合EncryptTransaction注解
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:38
 **/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}

定义EncryptTransaction注解

import java.lang.annotation.*;
/**
 * 该注解有两种使用方式
 * ①:配合@SensitiveData加在类中的字段上
 * ②:直接在Mapper中的方法参数上使用
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:40
 **/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}

加解密工具类

解密接口

package sicnu.cs.ich.common.interceptor.transaction.service;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:43
 **/
public interface IDecryptUtil {
    /**
     * 解密
     *
     * @param result resultType的实例
     * @return T
     * @throws IllegalAccessException 字段不可访问异常
     */
    <T> T decrypt(T result) throws IllegalAccessException;
}

加密接口

package sicnu.cs.ich.common.interceptor.transaction.service;
import java.lang.reflect.Field;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:41
 **/
public interface IEncryptUtil {
    /**
     * 加密
     *
     * @param declaredFields 加密字段
     * @param paramsObject   对象
     * @param <T>            入参类型
     * @return 返回加密
     * @throws IllegalAccessException 不可访问
     */
    <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}
package sicnu.cs.ich.common.interceptor.transaction.service.impl;
import org.springframework.stereotype.Component;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;
import java.lang.reflect.Field;
import java.util.Objects;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:43
 **/
@Component
public class DecryptImpl implements IDecryptUtil {
    /**
     * 解密
     *
     * @param result resultType的实例
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被DecryptTransaction注解的字段
            EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
            if (!Objects.isNull(encryptTransaction)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    try {
                        field.set(result, DBAESUtil.decrypt(value));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return result;
    }
}

加密实现类

package sicnu.cs.ich.common.interceptor.transaction.service.impl;
import com.fasterxml.jackson.databind.ObjectReader;
import org.springframework.stereotype.Component;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:42
 **/
@Component
public class EncryptUtilImpl implements IEncryptUtil {
    @Override
    public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
            //取出所有被EncryptTransaction注解的字段
        for (Field field : declaredFields) {
            EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
            if (!Objects.isNull(encryptTransaction)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);
                //暂时只实现String类型的加密
                if (object instanceof String) {
                    String value = (String) object;
                    //加密
                    try {
                        field.set(paramsObject, DBAESUtil.encrypt(value));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return paramsObject;
    }
}

解密实现类

package sicnu.cs.ich.common.interceptor.transaction.service.impl;
import org.springframework.stereotype.Component;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;
import java.lang.reflect.Field;
import java.util.Objects;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:43
 **/
@Component
public class DecryptImpl implements IDecryptUtil {
    /**
     * 解密
     *
     * @param result resultType的实例
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被DecryptTransaction注解的字段
            EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
            if (!Objects.isNull(encryptTransaction)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    try {
                        field.set(result, DBAESUtil.decrypt(value));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return result;
    }
}

加解密工具类

package sicnu.cs.ich.common.util.keyCryptor;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:45
 **/
public class DBAESUtil {
    private static final String DEFAULT_V = "6859505890402435";
    // 自己填写
    private static final String KEY = "***";
    private static final String ALGORITHM = "AES";
    private static SecretKeySpec getKey() {
        byte[] arrBTmp = DBAESUtil.KEY.getBytes();
        // 创建一个空的16位字节数组(默认值为0)
        byte[] arrB = new byte[16];
        for (int i = 0; i < arrBTmp.length && i < arrB.length; i  ) {
            arrB[i] = arrBTmp[i];
        }
        return new SecretKeySpec(arrB, ALGORITHM);
    }
    /**
     * 加密
     */
    public static String encrypt(String content) throws Exception {
        final Base64.Encoder encoder = Base64.getEncoder();
        SecretKeySpec keySpec = getKey();
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes());
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
        byte[] encrypted = cipher.doFinal(content.getBytes());
        return encoder.encodeToString(encrypted);
    }
    /**
     * 解密
     */
    public static String decrypt(String content) throws Exception {
        final Base64.Decoder decoder = Base64.getDecoder();
        SecretKeySpec keySpec = getKey();
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes());
        cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
        byte[] base64 = decoder.decode(content);
        byte[] original = cipher.doFinal(base64);
        return new String(original);
    }
}

插件实现

参数插件ParameterInterceptor

切入mybatis设置参数时对敏感数据进行加密

Mybatis插件的使用就是通过实现Mybatis中的Interceptor接口

再配合@Intercepts注解

// 使用mybatis插件时需要定义签名
// type标识需要切入的Handler
// method表示要要切入的方法
@Intercepts({
@Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class),
})
package sicnu.cs.ich.common.interceptor.transaction;
import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData;
import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil;
import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.PreparedStatement;
import java.util.*;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-22:48
 **/
@Slf4j
// 注入Spring
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class ParameterInterceptor implements Interceptor {
    @Autowired
    private IEncryptUtil IEncryptUtil;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        MybatisParameterHandler parameterHandler = (MybatisParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        //取出实例
        Object parameterObject = parameterField.get(parameterHandler);
        // 搜索该方法中是否有需要加密的普通字段
        List<String> paramNames = searchParamAnnotation(parameterHandler);
        if (parameterObject != null) {
            Class<?> parameterObjectClass = parameterObject.getClass();
            //对类字段进行加密
            //校验该实例的类是否被@SensitiveData所注解
            SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
            if (Objects.nonNull(sensitiveData)) {
                //取出当前当前类所有字段,传入加密方法
                Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                IEncryptUtil.encrypt(declaredFields, parameterObject);
            }
            // 对普通字段进行加密
            if (!CollectionUtils.isEmpty(paramNames)) {
                // 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射
                Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
                boundSqlField.setAccessible(true);
                BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
                PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
                // 改写参数
                processParam(parameterObject, paramNames);
                // 改写的参数设置到原parameterHandler对象
                parameterField.set(parameterHandler, parameterObject);
                parameterHandler.setParameters(ps);
            }
        }
        return invocation.proceed();
    }
    private void processParam(Object parameterObject, List<String> params) throws Exception {
        // 处理参数对象  如果是 map 且map的key 中没有 tenantId,添加到参数map中
        // 如果参数是bean,反射设置值
        if (parameterObject instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, String> map = ((Map<String, String>) parameterObject);
            for (String param : params) {
                String value = map.get(param);
                map.put(param, value==null?null:DBAESUtil.encrypt(value));
            }
//            parameterObject = map;
        }
    }
    private List<String> searchParamAnnotation(ParameterHandler parameterHandler) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
        Class<MybatisParameterHandler> handlerClass = MybatisParameterHandler.class;
        Field mappedStatementFiled = handlerClass.getDeclaredField("mappedStatement");
        mappedStatementFiled.setAccessible(true);
        MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler);
        String methodName = mappedStatement.getId();
        Class<?> mapperClass = Class.forName(methodName.substring(0, methodName.lastIndexOf('.')));
        methodName = methodName.substring(methodName.lastIndexOf('.')   1);
        Method[] methods = mapperClass.getDeclaredMethods();
        Method method = null;
        for (Method m : methods) {
            if (m.getName().equals(methodName)) {
                method = m;
                break;
            }
        }
        List<String> paramNames = null;
        if (method != null) {
            Annotation[][] pa = method.getParameterAnnotations();
            Parameter[] parameters = method.getParameters();
            for (int i = 0; i < pa.length; i  ) {
                for (Annotation annotation : pa[i]) {
                    if (annotation instanceof EncryptTransaction) {
                        if (paramNames == null) {
                            paramNames = new ArrayList<>();
                        }
                        paramNames.add(parameters[i].getName());
                    }
                }
            }
        }
        return paramNames;
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

返回值插件ResultSetInterceptor

package sicnu.cs.ich.common.interceptor.transaction;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;
/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/10/26-23:02
 **/
@Slf4j
@Component
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {
    @Autowired
    private sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil IDecryptUtil;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            @SuppressWarnings("unchecked")
            ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject;
            if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    IDecryptUtil.decrypt(result);
                }
            }
            //基于selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                IDecryptUtil.decrypt(resultObject);
            }
        }
        return resultObject;
    }
    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
    }
}

使用

注解在实体类上

import lombok.*;
import org.springframework.security.core.userdetails.UserDetails;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData;
@With
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@SensitiveData // 插件只对加了该注解的类进行扫描,只有加了这个注解的类才会生效
public class User implements Serializable {
    private Integer id;
    private String username;
    private String openId;
    private String password;
    // 表明对该字段进行加密
    @EncryptTransaction
    private String email;
    // 表明对该字段进行加密
    @EncryptTransaction
    private String mobile;
    private Date createTime;
    private Date expireTime;
    private Boolean status = true;
}

注解在参数上

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction;
@Mapper
public interface UserMapper extends BaseMapper<User> {
   // 只需要在参数前加上@EncryptTransaction 即可
   long countByEmail(@EncryptTransaction @Param("email") String email);
   long countByMobile(@EncryptTransaction @Param("mobile") String mobile);
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持Devmax。

Mybatis插件+注解实现数据脱敏方式的更多相关文章

  1. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  2. 一种angular的方法级的缓存注解(装饰器)

    本篇文章主要介绍了一种angular的方法级的缓存注解(装饰器),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  3. Spring详细讲解@Autowired注解

    @Autowired注解可以用在类属性,构造函数,setter方法和函数参数上,该注解可以准确地控制bean在何处如何自动装配的过程。在默认情况下,该注解是类型驱动的注入

  4. Springboot集成mybatis实现多数据源配置详解流程

    在日常开发中,若遇到多个数据源的需求,怎么办呢?通过springboot集成mybatis实现多数据源配置,简单尝试一下,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  5. 详解springboot测试类注解

    这篇文章主要介绍了springboot测试类注解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. @FeignClient注解中属性contextId的使用说明

    这篇文章主要介绍了@FeignClient注解中属性contextId的使用说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  7. Mybatis结果集映射与生命周期详细介绍

    结果集映射指的是将数据表中的字段与实体类中的属性关联起来,这样 MyBatis 就可以根据查询到的数据来填充实体对象的属性,帮助我们完成赋值操作

  8. Spring Boot 整合持久层之MyBatis

    在实际开发中不仅仅是要展示数据,还要构成数据模型添加数据,这篇文章主要介绍了SpringBoot集成Mybatis操作数据库,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  9. Mybatis详解动态SQL以及单表多表查询的应用

    MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑,下面这篇文章主要给大家介绍了关于Mybatis超级强大的动态SQL语句的相关资料,需要的朋友可以参考下

  10. mybatis水平分表实现动态表名的项目实例

    本文主要介绍了mybatis水平分表实现动态表名的项目实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部