一、项目介绍
1、项目背景
有时候我们数据库中存储一些敏感的信息比如手机号、银行卡号,我们希望我们查询出来的的时候对一些敏感信息做一些脱敏处理。
当面项目是基于自定义Mybatis插件方式实现数据脱敏处理,通过插件拦截结果集进行脱敏后再返回,所以对于使用者透明,业务逻辑无感知。
目前支持用户名脱敏、手机号脱敏、座机号码脱敏、银行卡脱敏、身份证号脱敏、邮箱脱敏、地址脱敏。
2、注解说明
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#75715e">/**
* 对需要脱敏的字段加上该注解
*/</span>
<span style="color:#75715e">@Documented</span>
<span style="color:#75715e">@Inherited</span>
<span style="color:#75715e">@Retention(RetentionPolicy.RUNTIME)</span>
<span style="color:#75715e">@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})</span>
<span style="color:#f92672">public</span> <span style="color:#75715e">@interface</span> SensitiveField {
<span style="color:#75715e">/**
* 脱敏类型
*/</span>
SensitiveTypeEnums <span style="color:#a6e22e">value</span><span style="color:#f8f8f2">()</span>;
<span style="color:#75715e">/**
* 填充值
*/</span>
String <span style="color:#a6e22e">fillValue</span><span style="color:#f8f8f2">()</span> <span style="color:#f92672">default</span> <span style="color:#e6db74">"*"</span>;
}
</code></span></span>
目前支持脱敏类型
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#75715e">/**
* 脱敏类型
*/</span>
<span style="color:#f92672">public</span> <span style="color:#f92672">enum</span> <span style="color:#a6e22e">SensitiveTypeEnums</span> {
<span style="color:#75715e">/**
* 默认方式脱敏
*/</span>
DEFAULT(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">6</span>),
<span style="color:#75715e">/**
* 中文名称
*/</span>
CHINESE_NAME(<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">1</span>),
<span style="color:#75715e">/**
* 手机号
*/</span>
MOBILE(<span style="color:#ae81ff">3</span>,<span style="color:#ae81ff">4</span>),
<span style="color:#75715e">/**
* 座机号码
*/</span>
FIXED_PHONE(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">4</span>),
<span style="color:#75715e">/**
* 银行卡
*/</span>
BANK_CARD(<span style="color:#ae81ff">6</span>,<span style="color:#ae81ff">4</span>),
<span style="color:#75715e">/**
* 身份证号
*/</span>
ID_CARD(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">4</span>),
<span style="color:#75715e">/**
* 邮箱
*/</span>
EMAIL(<span style="color:#ae81ff">2</span>,<span style="color:#ae81ff">0</span>),
<span style="color:#75715e">/**
* 地址
*/</span>
ADDRESS(<span style="color:#ae81ff">6</span>,<span style="color:#ae81ff">4</span>),
;
}
</code></span></span>
举例
: 如11位的手机号,默认脱敏策略是显示(3,4)前三后四,如 13712345678 脱敏后变成 137****5678。
二、实现原理
1、该插件项目的原理
通过拦截器拦截Mybatis的select
语句,通过自定义注解
获取到需要脱敏处理的属性字段,并为该属性根据不同的脱敏类型进行脱敏,处理后再返回。
2、插件源码
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#75715e">/**
* 基于拦截器对数据脱敏
*
* <span style="color:#808080">@author</span> xub
* <span style="color:#808080">@date</span> 2022/6/2 下午2:23
*/</span>
<span style="color:#75715e">@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {java.sql.Statement.class})
})</span>
<span style="color:#f92672">public</span> <span style="color:#f92672">class</span> <span style="color:#a6e22e">DesensitizationInterceptor</span> <span style="color:#f92672">implements</span> <span style="color:#a6e22e">Interceptor</span> {
<span style="color:#f92672">private</span> <span style="color:#f92672">static</span> <span style="color:#f92672">final</span> <span style="color:#e6db74">Logger</span> <span style="color:#e6db74">log</span> <span style="color:#ab5656">=</span> LoggerFactory.getLogger(DesensitizationInterceptor.class);
<span style="color:#75715e">/**
* key值为class对象 value可以理解成是该类带有SensitiveField注解的属性,只不过对属性封装了一层。
* 它是非常能够提高性能的处理器 它的作用就是不用每一次一个对象经来都要看下它的哪些属性带有SensitiveField注解
* 毕竟类的反射在性能上并不友好。只要key包含该对象那就不需要检查它哪些属性带SensitiveField注解。
*/</span>
<span style="color:#f92672">private</span> Map<Class, List<Handler>> handlerMap = <span style="color:#f92672">new</span> <span style="color:#a6e22e">ConcurrentHashMap</span><>();
<span style="color:#75715e">@Override</span>
<span style="color:#f92672">public</span> Object <span style="color:#a6e22e">intercept</span><span style="color:#f8f8f2">(Invocation invocation)</span> <span style="color:#f92672">throws</span> Throwable {
<span style="color:#75715e">// 获取结果</span>
List<Object> results = (List<Object>) invocation.proceed();
<span style="color:#f92672">if</span> (CollectionUtils.isEmpty(results)) {
<span style="color:#f92672">return</span> results;
}
<span style="color:#75715e">// 批量设置加密</span>
<span style="color:#f92672">for</span> (Object object : results) {
process(object);
}
<span style="color:#f92672">return</span> results;
}
<span style="color:#f92672">private</span> <span style="color:#f92672">void</span> <span style="color:#a6e22e">process</span><span style="color:#f8f8f2">(Object object)</span> <span style="color:#f92672">throws</span> Throwable {
<span style="color:#e6db74">Class</span> <span style="color:#e6db74">handlerKey</span> <span style="color:#ab5656">=</span> object.getClass();
List<Handler> handlerList = handlerMap.get(handlerKey);
<span style="color:#75715e">//性能优化点,如果有两个都是user对象同时,那么只需有个进行反射处理属性就好了,另一个只需执行下面的for循环</span>
SYNC:
<span style="color:#f92672">if</span> (handlerList == <span style="color:#ae81ff">null</span>) {
<span style="color:#f92672">synchronized</span> (<span style="color:#e6db74">this</span>) {
handlerList = handlerMap.get(handlerKey);
<span style="color:#75715e">//如果到这里map集合已经存在,则跳出到指定SYNC标签</span>
<span style="color:#f92672">if</span> (handlerList != <span style="color:#ae81ff">null</span>) {
<span style="color:#f92672">break</span> SYNC;
}
handlerMap.put(handlerKey, handlerList = <span style="color:#f92672">new</span> <span style="color:#a6e22e">ArrayList</span><>());
<span style="color:#75715e">// 反射工具类 获取带有SensitiveField注解的所有属性字段</span>
Set<Field> allFields = ReflectionUtils.getAllFields(
object.getClass(),
input -> input != <span style="color:#ae81ff">null</span> && input.getAnnotation(SensitiveField.class) != <span style="color:#ae81ff">null</span>
);
<span style="color:#f92672">for</span> (Field field : allFields) {
handlerList.add(<span style="color:#f92672">new</span> <span style="color:#a6e22e">Handler</span>(field));
}
}
}
<span style="color:#f92672">for</span> (Handler handler : handlerList) {
handler.accept(object);
}
}
<span style="color:#75715e">@Override</span>
<span style="color:#f92672">public</span> Object <span style="color:#a6e22e">plugin</span><span style="color:#f8f8f2">(Object target)</span> {
<span style="color:#f92672">return</span> Plugin.wrap(target, <span style="color:#e6db74">this</span>);
}
<span style="color:#75715e">@Override</span>
<span style="color:#f92672">public</span> <span style="color:#f92672">void</span> <span style="color:#a6e22e">setProperties</span><span style="color:#f8f8f2">(Properties properties)</span> {
}
<span style="color:#f92672">private</span> <span style="color:#f92672">static</span> <span style="color:#f92672">class</span> <span style="color:#a6e22e">Handler</span> {
Field field;
Handler(Field field) {
<span style="color:#e6db74">this</span>.field = field;
}
<span style="color:#f92672">private</span> <span style="color:#e6db74">boolean</span> <span style="color:#a6e22e">checkField</span><span style="color:#f8f8f2">(Object object, Field field)</span> <span style="color:#f92672">throws</span> IllegalAccessException {
<span style="color:#f92672">if</span> (!field.isAccessible()) {
field.setAccessible(<span style="color:#ae81ff">true</span>);
}
<span style="color:#75715e">//如果为空 那么就不用进行脱敏操作了</span>
<span style="color:#f92672">return</span> field.get(object) != <span style="color:#ae81ff">null</span>;
}
<span style="color:#f92672">public</span> <span style="color:#f92672">void</span> <span style="color:#a6e22e">accept</span><span style="color:#f8f8f2">(Object o)</span> <span style="color:#f92672">throws</span> Throwable {
<span style="color:#f92672">if</span> (checkField(o, field)) {
<span style="color:#e6db74">SensitiveField</span> <span style="color:#e6db74">annotation</span> <span style="color:#ab5656">=</span> field.getAnnotation(SensitiveField.class);
<span style="color:#e6db74">SensitiveTypeEnums</span> <span style="color:#e6db74">typeEnums</span> <span style="color:#ab5656">=</span> annotation.value();
<span style="color:#e6db74">String</span> <span style="color:#e6db74">fillValue</span> <span style="color:#ab5656">=</span> annotation.fillValue();
<span style="color:#e6db74">Object</span> <span style="color:#e6db74">o1</span> <span style="color:#ab5656">=</span> field.get(o);
log.info(<span style="color:#e6db74">"加密之前数据 = {}"</span>,o1);
<span style="color:#e6db74">SensitiveStrategy</span> <span style="color:#e6db74">sensitiveStrategy</span> <span style="color:#ab5656">=</span> SensitiveContext.get(typeEnums);
<span style="color:#e6db74">String</span> <span style="color:#e6db74">s</span> <span style="color:#ab5656">=</span> sensitiveStrategy.handle(o1, fillValue);
log.info(<span style="color:#e6db74">"加密之后数据 = {}"</span>,s);
field.set(o, s);
}
}
}
}
</code></span></span>
三、使用方式
1、添加jar包
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-xml"> <span style="color:#f8f8f2"><<span style="color:#f92672 !important">dependency</span>></span>
<span style="color:#f8f8f2"><<span style="color:#f92672 !important">groupId</span>></span>com.jincou<span style="color:#f8f8f2"></<span style="color:#f92672 !important">groupId</span>></span>
<span style="color:#f8f8f2"><<span style="color:#f92672 !important">artifactId</span>></span>mybatis-sensitive-plugin-starter<span style="color:#f8f8f2"></<span style="color:#f92672 !important">artifactId</span>></span>
<span style="color:#f8f8f2"><<span style="color:#f92672 !important">version</span>></span>1.0.0<span style="color:#f8f8f2"></<span style="color:#f92672 !important">version</span>></span>
<span style="color:#f8f8f2"></<span style="color:#f92672 !important">dependency</span>></span>
</code></span></span>
2、User实体添加注解
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#75715e">/**
* 用户信息表
*/</span>
<span style="color:#75715e">@Data</span>
<span style="color:#f92672">public</span> <span style="color:#f92672">class</span> <span style="color:#a6e22e">User</span> <span style="color:#f92672">implements</span> <span style="color:#a6e22e">Serializable</span> {
<span style="color:#f92672">private</span> <span style="color:#f92672">static</span> <span style="color:#f92672">final</span> <span style="color:#e6db74">long</span> <span style="color:#e6db74">serialVersionUID</span> <span style="color:#ab5656">=</span> <span style="color:#ae81ff">1L</span>;
<span style="color:#75715e">/**
* 主键
*/</span>
<span style="color:#f92672">private</span> Integer id;
<span style="color:#75715e">/**
* 姓名
*/</span>
<span style="color:#75715e">@SensitiveField(SensitiveTypeEnums.CHINESE_NAME)</span>
<span style="color:#f92672">private</span> String name;
<span style="color:#75715e">/**
* 邮箱
*/</span>
<span style="color:#75715e">@SensitiveField(SensitiveTypeEnums.EMAIL)</span>
<span style="color:#f92672">private</span> String email;
<span style="color:#75715e">/**
* 手机号
*/</span>
<span style="color:#75715e">@SensitiveField(SensitiveTypeEnums.MOBILE)</span>
<span style="color:#f92672">private</span> String mobile;
<span style="color:#75715e">/**
* 地址
*/</span>
<span style="color:#75715e">@SensitiveField(SensitiveTypeEnums.ADDRESS)</span>
<span style="color:#f92672">private</span> String address;
}
</code></span></span>
3、请求接口
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#75715e">@RestController</span>
<span style="color:#f92672">public</span> <span style="color:#f92672">class</span> <span style="color:#a6e22e">TestController</span> {
<span style="color:#75715e">@Autowired</span>
<span style="color:#f92672">private</span> UserMapper userMapper;
<span style="color:#75715e">@GetMapping("/getById")</span>
User <span style="color:#a6e22e">get</span><span style="color:#f8f8f2">(Integer id)</span>{
<span style="color:#f92672">return</span> userMapper.selectByPrimaryKey(id);
}
<span style="color:#75715e">@GetMapping("/findAllUser")</span>
List<User> <span style="color:#a6e22e">findAllUser</span><span style="color:#f8f8f2">()</span>{
<span style="color:#f92672">return</span> userMapper.findAllUser();
}
}
</code></span></span>
请求接口url
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-bash">http://localhost:8080/findAllUser
</code></span></span>
4、接口返回数据
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-json">[
{
<span style="color:#f92672">"id"</span>:<span style="color:#ae81ff">1</span>,
<span style="color:#f92672">"age"</span>:<span style="color:#ae81ff">10</span>,
<span style="color:#f92672">"name"</span>:<span style="color:#e6db74">"z*****u"</span>,
<span style="color:#f92672">"email"</span>:<span style="color:#e6db74">"45*******@qq.com"</span>,
<span style="color:#f92672">"mobile"</span>:<span style="color:#e6db74">"137****2222"</span>,
<span style="color:#f92672">"address"</span>:<span style="color:#e6db74">"宁波市慈溪市***********鸣鹤古镇"</span>
},
{
<span style="color:#f92672">"id"</span>:<span style="color:#ae81ff">2</span>,
<span style="color:#f92672">"age"</span>:<span style="color:#ae81ff">20</span>,
<span style="color:#f92672">"name"</span>:<span style="color:#e6db74">"l**i"</span>,
<span style="color:#f92672">"email"</span>:<span style="color:#e6db74">"xu****@outlook.com"</span>,
<span style="color:#f92672">"mobile"</span>:<span style="color:#e6db74">"139****5678"</span>,
<span style="color:#f92672">"address"</span>:<span style="color:#e6db74">"西安市未央区************100米"</span>
},
{
<span style="color:#f92672">"id"</span>:<span style="color:#ae81ff">8</span>,
<span style="color:#f92672">"age"</span>:<span style="color:#ae81ff">30</span>,
<span style="color:#f92672">"name"</span>:<span style="color:#e6db74">"w****a"</span>,
<span style="color:#f92672">"email"</span>:<span style="color:#e6db74">"wa****@163.com"</span>,
<span style="color:#f92672">"mobile"</span>:<span style="color:#e6db74">"137****5678"</span>,
<span style="color:#f92672">"address"</span>:<span style="color:#e6db74">"西安市未央区************100米"</span>
}
]
</code></span></span>
5、表中原始数据
<span style="color:#4b4b4b"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#ae81ff">1</span> <span style="color:#ae81ff">10</span> zhaoliu <span style="color:#ae81ff">450760999</span><span style="color:#75715e">@qq</span>.com <span style="color:#ae81ff">13722222222</span> 宁波市慈溪市观海卫镇禹皇路<span style="color:#ae81ff">999</span>号鸣鹤古镇
<span style="color:#ae81ff">2</span> <span style="color:#ae81ff">20</span> lisi xu5555<span style="color:#75715e">@outlook</span>.com <span style="color:#ae81ff">13912345678</span> 西安市未央区凤城二路与连心路交叉口南<span style="color:#ae81ff">100</span>米
<span style="color:#ae81ff">8</span> <span style="color:#ae81ff">30</span> wangba wangba@<span style="color:#ae81ff">163.</span>com <span style="color:#ae81ff">13712345678</span> 西安市未央区凤城二路与连心路交叉口南<span style="color:#ae81ff">100</span>米
</code></span></span>