今天遇到个需求需要对现有的数据进行脱敏处理。于是简单研究了下。
其实拦截器对脱敏处理主要处理两种数据,一种是bean类型,一种是map类型。
普通的javabean利用注解+反射来处理,map的数据自己维护需要脱敏的key以及规则。bean类型是用mybatis以及mybatis-plus自动生成的SQL映射的;map类型是手写的返回map类型的SQL和mybatis-plus的返回map类型的数据。
1.主要代码如下:
1.注解
package cn.xm.exam.mybatis; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Desensitization { DesensitionType type(); String[] attach() default ""; }
2.定义枚举脱敏规则
package cn.xm.exam.mybatis; public enum DesensitionType { PHONE("phone", "11位手机号", "^(\\d{3})\\d{4}(\\d{4})$", "$1****$2"), // ID_CARD("idCard", "16或者18身份证号", "^(\\d{4})\\d{8,10}(\\w{4})$", // "$1****$2"), ID_CARD("idCard", "16或者18身份证号", "^(\\d{4})\\d{11,13}(\\w{1})$", "$1****$2"), BANK_CARD("bankCardNo", "银行卡号", "^(\\d{4})\\d*(\\d{4})$", "$1****$2"), ADDRESS("addrss", "地址", "(?<=.{3}).*(?=.{3})", "*"), REAL_NAME( "realName", "真实姓名", "(?<=.{1}).*(?=.{1})", "*"), EMAIL("email", "电子邮箱", "(\\w+)\\w{5}@(\\w+)", "$1***@$2"), CUSTOM("custom", "自定义正则处理", ""), TRUNCATE("truncate", "字符串截取处理", ""); private String type; private String describe; private String[] regular; DesensitionType(String type, String describe, String... regular) { this.type = type; this.describe = describe; this.regular = regular; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getDescribe() { return describe; } public void setDescribe(String describe) { this.describe = describe; } public String[] getRegular() { return regular; } public void setRegular(String[] regular) { this.regular = regular; } }
3.增加mybatis拦截器
package cn.xm.exam.mybatis; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @SuppressWarnings({ "rawtypes", "unchecked" }) @Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }), }) public class DesensitizationInterceptor implements Interceptor { private static final Logger logger = LoggerFactory.getLogger(DesensitizationInterceptor.class); private boolean desensitization = false;// 脱敏 private static final Map<String, DesensitionType> desensitionMap = new LinkedHashMap<>(); static { initDensensitionMap(); } @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); // 如果需要对结果脱敏,则执行 if (desensitization) { // 先对Map进行处理 if (result != null && result instanceof Map) { return this.desensitizationMap(result); } // 处理集合 if (result instanceof ArrayList<?>) { List<?> list = (ArrayList<?>) result; return this.desensitization(list); } // 处理单个bean return this.desensitization(result); } return result; } private static void initDensensitionMap() { desensitionMap.put("idCode", DesensitionType.ID_CARD); desensitionMap.put("idCard", DesensitionType.ID_CARD); desensitionMap.put("userIDCard", DesensitionType.ID_CARD); desensitionMap.put("userIdCard", DesensitionType.ID_CARD); desensitionMap.put("username", DesensitionType.REAL_NAME); desensitionMap.put("address", DesensitionType.ADDRESS); } /* * 对map脱敏 */ private Object desensitizationMap(Object result) { Map mapResult = (Map) result; if (MapUtils.isEmpty(mapResult)) { return mapResult; } Set<String> keySet = mapResult.keySet(); for (String key : keySet) { if (desensitionMap.containsKey(key)) { DesensitionType desensitionType = desensitionMap.get(key); String replacedVal = getReplacedVal(desensitionType, MapUtils.getString(mapResult, key), null); mapResult.put(key, replacedVal); } } return result; } private List desensitization(List list) { if (CollectionUtils.isEmpty(list)) { return Collections.emptyList(); } Class cls = null; for (Object o : list) { // 脱敏map,改变引用地址(根据静态配置脱敏) if (o != null && o instanceof Map) { o = desensitizationMap(o); continue; } // 脱敏bean(根据注解脱敏) if (cls == null) { cls = o.getClass(); } o = desensitization(o); } return list; } @Override public Object plugin(Object target) { // TODO Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties // 方法,就不会初始化,因此考虑在这个方法中做一次判断和初始化 return Plugin.wrap(target, this); } /** * 用于在Mybatis配置文件中指定一些属性的,注册当前拦截器的时候可以设置一些属性 */ @Override public void setProperties(Properties properties) { } private Object desensitization(Object obj) { if (obj == null) { return obj; } Class cls = obj.getClass(); Field[] objFields = cls.getDeclaredFields(); if (ArrayUtils.isEmpty(objFields)) { return obj; } for (Field field : objFields) { if ("serialVersionUID".equals(field.getName())) { continue; } Desensitization desensitization = null; if (String.class != field.getType() || (desensitization = field.getAnnotation(Desensitization.class)) == null) { continue; } try { field.setAccessible(true); String value = field.get(obj) != null ? field.get(obj).toString() : null; if (StringUtils.isBlank(value)) { continue; } value = getReplacedVal(desensitization.type(), value, desensitization.attach()); field.set(obj, value); } catch (Exception ignore) { ignore.printStackTrace(); } } return obj; } private String getReplacedVal(DesensitionType type, String value, String[] attachs) { List<String> regular = null; switch (type) { case CUSTOM: regular = Arrays.asList(attachs); break; case TRUNCATE: regular = truncateRender(attachs); break; default: regular = Arrays.asList(type.getRegular()); } if (regular != null && regular.size() > 1) { String match = regular.get(0); String result = regular.get(1); if (null != match && result != null && match.length() > 0) { value = ((String) value).replaceAll(match, result); return value; } } return ""; } private List<String> truncateRender(String[] attachs) { List<String> regular = new ArrayList<>(); if (null != attachs && attachs.length > 1) { String rule = attachs[0]; String size = attachs[1]; String template, result; if ("0".equals(rule)) { template = "^(\\S{%s})(\\S+)$"; result = "$1"; } else if ("1".equals(rule)) { template = "^(\\S+)(\\S{%s})$"; result = "$2"; } else { return regular; } try { if (Integer.parseInt(size) > 0) { regular.add(0, String.format(template, size)); regular.add(1, result); } } catch (Exception e) { logger.warn("ValueDesensitizeFilter truncateRender size {} exception", size); } } return regular; } public boolean isDesensitization() { return desensitization; } public void setDesensitization(boolean desensitization) { this.desensitization = desensitization; } }
解释:
(1)Interceptor接口有三个方法,如下:
package org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
intercept方法中编写我们自己的处理逻辑。类似于AOP。
(2)@Intercepts注解:
package org.apache.ibatis.plugin; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author Clinton Begin */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Intercepts { Signature[] value(); }
Intercepts注解需要一个Signature(拦截点)参数数组。通过Signature来指定拦截哪个对象里面的哪个方法。
(3)Signature注解指定需要拦截那个类对象的哪个方法
package org.apache.ibatis.plugin; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Signature { Class<?> type(); String method(); Class<?>[] args(); }
class:指定定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个。
method:指定拦截的方法,方法名字即可
args:指定拦截的方法对应的参数,JAVA里面方法可能重载,不指定参数,不能确定调用那个方法。
4.mybatis的sqlSessionFactory中注册拦截器
<!--2. 配置 Mybatis的会话工厂 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 数据源 --> <property name="dataSource" ref="dataSource" /> <!-- 配置Mybatis的核心 配置文件所在位置 --> <property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml" /> <!-- 注意其他配置 --> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <!--使用下面的方式配置参数,一行配置一个 --> <value> helperDialect=mysql reasonable=true </value> </property> </bean> <bean class="cn.xm.exam.mybatis.DesensitizationInterceptor"> <property name="desensitization" value="true"></property> </bean> </array> </property> </bean>
5.Javabean中增加注解,如果是查询返回的map,会根据desensitionMap的规则进行脱敏
public class EmployeeIn { private String employeeid; /** * 员工编号 */ private String employeenumber; private String name; /** * 身份证号 */ @Desensitization(type = DesensitionType.ID_CARD) private String idcode; ... }
至此就可以实现一些基本的数据脱敏,前台查看返回的信息如下:
2.原理简单介绍
Mybatis拦截器设计的初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。通过Mybatis拦截器我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。
Mybatis里面的核心对象还是比较多的,如下:
Mybatis拦截器并不是每个对象里面的方法都可以被拦截的。Mybatis拦截器只能拦截Executor、ParameterHandler、StatementHandler、ResultSetHandler四个对象里面的方法。
1.Executor接口如下:
/** <a href="http://www.cpupk.com/decompiler">Eclipse Class Decompiler</a> plugin, Copyright (c) 2017 Chen Chao. */ package org.apache.ibatis.executor; import java.sql.SQLException; import java.util.List; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.transaction.Transaction; /** * @author Clinton Begin */ public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement ms, Object parameter) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; List<BatchResult> flushStatements() throws SQLException; void commit(boolean required) throws SQLException; void rollback(boolean required) throws SQLException; CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql); boolean isCached(MappedStatement ms, CacheKey key); void clearLocalCache(); void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType); Transaction getTransaction(); void close(boolean forceRollback); boolean isClosed(); void setExecutorWrapper(Executor executor); }
Mybatis中所有的Mapper语句的执行都是通过Executor进行的。Executor是Mybatis的核心接口。从其定义的接口方法我们可以看出,对应的增删改语句是通过Executor接口的update方法进行的,查询是通过query方法进行的。
2.ParameterHandler
/** <a href="http://www.cpupk.com/decompiler">Eclipse Class Decompiler</a> plugin, Copyright (c) 2017 Chen Chao. */ package org.apache.ibatis.executor.parameter; import java.sql.PreparedStatement; import java.sql.SQLException; /** * A parameter handler sets the parameters of the {@code PreparedStatement} * * @author Clinton Begin */ public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement ps) throws SQLException; }
ParameterHandler用来设置参数规则,当StatementHandler使用prepare()方法后,接下来就是使用它来设置参数。所以如果有对参数做自定义逻辑处理的时候,可以通过拦截ParameterHandler来实现。
3.StatementHandler
/** <a href="http://www.cpupk.com/decompiler">Eclipse Class Decompiler</a> plugin, Copyright (c) 2017 Chen Chao. */ package org.apache.ibatis.executor.statement; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.List; import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.session.ResultHandler; /** * @author Clinton Begin */ public interface StatementHandler { Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; void parameterize(Statement statement) throws SQLException; void batch(Statement statement) throws SQLException; int update(Statement statement) throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(Statement statement) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
StatementHandler负责处理Mybatis与JDBC之间Statement的交互。
4.ResultSetHandler
/** <a href="http://www.cpupk.com/decompiler">Eclipse Class Decompiler</a> plugin, Copyright (c) 2017 Chen Chao. */ package org.apache.commons.dbutils; import java.sql.ResultSet; import java.sql.SQLException; public interface ResultSetHandler<T> { T handle(ResultSet rs) throws SQLException; }
ResultSetHandler用于对查询到的结果做处理。所以如果你有需求需要对返回结果做特殊处理的情况下可以去拦截ResultSetHandler的处理。
3.常见的拦截器
PageInterceptor 分页插件