乐趣区

关于java:求求你别乱脱敏了MyBatis-插件-注解轻松实现数据脱敏So-easy

问题

在我的项目中须要对用户敏感数据进行脱敏解决,例如身份号、手机号等信息进行加密再入库。

解决思路

  • 就是:一种最简略间接的形式,在所有波及数据敏感的查问到对插入时进行明码加解密
  • 办法二:有办法一到呈现对所有重大问题的影响,须要思考到问题的呈现,并且须要思考可能呈现的组员时增加数据的办法。

最初决定采纳 mybatis 的插件在 mybatis 的 SQL 执行和后果填充操作上进行切入。下层业务调用不再须要思考数据的加敏同时也保障了数据的加解密

Mybatis 插件原理

Mybatis 的是通过拦截器实现的,Mabatis 反对对当事人进行拦挡

实现

  • 设置对参数中带有敏感参数字段的数据时进行加密
  • 对返回的后果进行解密解决

依据不同的要求,咱们只须要对 ParameterHandlerResultSetHandler进行切入。

定义特定注解,在切入时须要查看字段中是否蕴含注解来是否加解密

加注解

定义 SensitiveData 注解

import java.lang.annotation.*;
/**
 * 该注解定义在类上
 * 插件通过扫描类对象是否蕴含这个注解来决定是否持续扫描其中的字段注解
 * 这个注解要配合 EncryptTransaction 注解
 **/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {}

定义 EncryptTransaction 注解

import java.lang.annotation.*;
/**
 * 该注解有两种应用形式
 * ①:配合 @SensitiveData 加在类中的字段上
 * ②:间接在 Mapper 中的办法参数上应用
 **/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {}

加解密工具类

解密

package sicnu.cs.ich.common.interceptor.transaction.service;

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;

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;

@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;

@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;

@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;

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);
    }

}

举荐一个开源收费的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

插件实现

参数插件 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.*;

@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;

@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);

}

版权申明:本文为 CSDN 博主「shenyang1026」的原创文章,遵循 CC 4.0 BY-SA 版权协定,转载请附上原文出处链接及本申明。

原文链接:https://blog.csdn.net/relosy/article/details/123494036

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版