当前位置: 首页 > news >正文

MyBatis 插件 + 注解轻松实现数据脱敏

问题

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

解决思路

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

  • 方法二:有方法一到出现对所有重大问题的影响,需要考虑到问题的出现,并且需要考虑可能出现的组员时添加数据的方法。

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

Mybatis 插件原理

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

实现

  • 设置对参数中带有敏感参数字段的数据时进行加密

  • 对返回的结果进行解密处理

根据不同的要求,我们只需要对ParameterHandlerResultSetHandler进行切入。定义特定注解,在切入时需要检查字段中是否包含注解来是否加解密。

另外,如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。

加注解

定义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的实例*/@Overridepublic <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 {@Overridepublic <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的实例*/@Overridepublic <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);}}

插件实现

参数插件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 {@Autowiredprivate IEncryptUtil IEncryptUtil;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler//若指定ResultSetHandler ,这里则能强转为ResultSetHandlerMybatisParameterHandler 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;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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 {@Autowiredprivate sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil IDecryptUtil;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//取出查询的结果Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}//基于selectListif (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);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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;// 表明对该字段进行加密@EncryptTransactionprivate String email;// 表明对该字段进行加密@EncryptTransactionprivate 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);}
http://www.lryc.cn/news/9372.html

相关文章:

  • MySQL优化篇-MySQL压力测试
  • CF43A Football 题解
  • Nginx常用命令及具体应用(Linux系统)
  • 从零实现Web服务器(三):日志优化,压力测试,实战接收HTTP请求,实战响应HTTP请求
  • MFC入门
  • 1、H5+CSS面试题
  • 亚马逊云科技重磅发布《亚马逊云科技汽车行业解决方案》
  • Springboot扩展点之FactoryBean
  • 新库上线 | CnOpenDataA股上市公司交易所监管措施数据
  • 同步辐射XAFS表征方法的应用场景分析
  • 06 antdesign react Anchor 不同页面之间实现锚点
  • mysql调优-内存缓冲池
  • 【LeetCode】每日一题(5)
  • 输入任意多个整数, 把这些数据保存到文件data.txt中.(按ctrl + z)
  • Mysql数据库的时间(3)一如何用函数插入时间
  • 关于eval函数(将JSON格式的字符串转换成JSON格式对象)
  • 2023最强软件测试面试题,精选100 道,内附答案版,冲刺金3银4
  • 一文搞懂Docker容器里进程的 pid 是如何申请出来的?
  • 若依框架如何新增自定义主题风格
  • C语言格式化输入和输出; Format格式化
  • Revit教程:怎么关掉工具栏的实时提示?
  • javascript 简介
  • 医学图象分割常用损失函数(附Pytorch和Keras代码)
  • 【新2023】华为OD机试 - 病菌感染(Python)
  • QGIS中进行批量坡向计算
  • Redis持久化机制
  • 2、VUE面试题
  • DeepSort:论文翻译
  • Debezium系列之:重置Sqlserver数据库的LSN拉取历史数据
  • 一起Talk Android吧(第四百九十四回:在Android中使用MQTT通信四)