IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 基于jdbc驱动拦截器从应用层面拦截敏感数据做脱敏加密处理 -> 正文阅读

[大数据]基于jdbc驱动拦截器从应用层面拦截敏感数据做脱敏加密处理

作者:token keyword

背景

最近思考到数据安全展示的另一个维度,怎么给客户用户带来价值?一个系统中希望以不同账户调用接口展示的敏感数据不同。之前已经通过端口代理的方式可以基于传输层面处理数据(修改请求修改返回报文),根据传输报文的格式解析实现对敏感数据的展示做脱敏处理,但是仔细回想这样的处理方式判断敏感信息是否需要脱敏加密维度只能是数据库的登录账户,客户端的ip,判断维度有点粗,适用于直接在客户连接整个库查询的场景。针对更细粒度的思考目前就是在服务中做一个基于jdbc的拦截器代理,查看了一下jdbc源码发现其中还是有很多我们日常使用中没有发现的财富是可以使用的,仔细思考了几种方案进行对比,这里与感兴趣的朋友一起分享下。

需求:

1.对原有系统的改动量不大。

2.在某些集团下的数据库都是用专属的db服务器统一存储的,对于一些资源信息,用户信息有存在公共库的情况,如服务器的基础登录信息,用户的基础信息等,按传统做法多个子系统之间都做数据同步,各自维护到自己库里面,一旦有信息变更多个数据库都需要进行修改,成本太大,那么用同一个库去存储控制读写权限就是一个比较好的选择了,但是这样的问题又来了,为了安全比如用户登录密码明文是不会直接出现在系统中的,几乎都是对比密文,而不同的系统加密方式又不一样,所以如果存储密文存在一定问题(暂时的解决方案是数据库中存明文,或者存储base64编译后的内容,因为安全不是绝对的,只能说內部相对安全)。

3.一个系统中有多种权限的管理员,如果是普通用户权限看其他人的信息敏感信息如薪水、身份证号、电话号码、等信息都是需要做脱敏处理的,但是拥有特殊权限的人应该是可以看到相应的明文的:

财务:可见薪水;

leader:可见电话号码;

负责社保的工作人员:可见身份证号。

方案对比分析:

要实现这样的统一处理功能肯定是需要在某一个地方统一管理的,实现切面和拦截器自然是首选,下面分析下常用的几种具体方案,查阅了下网上现有的方案几乎都是用框架自带拦截器或者注解去处理的吗,注解的方式改动成本太大了,自定义拦截器倒是一种不错选择,不过没法处理原生jdbc连接的查询。

方案1:使用端口代理的脱敏方式

无法满足需求,背景中已经详细介绍。

缺点:

1.对判断维度太粗了,只能精确到数据库用户和客户端ip,没法细化到登录用户。

2.端口代理不适用于一直跑的服务,一旦代理服务器出问题影响比较大。

3.数据源关系多了一层链路,传输效率有影响,且一旦代理地址链路发生更改客户端没有变更则影响无法预估。

方案2:具体业务具体对象自己处理

优点:

1.灵活,可以自定义实现功能。

缺点:

1.编码复杂,几乎是面向过程编程,所有的内容都修要自行编码处理达不到统一处理的效果。

2.代码分散,一旦有新的敏感信息表接入需要继续编码处理,如果需求变更了修改维护也比较麻烦。

方案3:mybatis-plus提供的mybatis-mate-sensitive-jackson

mybatis-plus提供的一个插件,很直接的一个原因就是要收费,而且必须用mybatis-plus,里面的字段还需要加很多的注解去实现,感觉还是比较麻烦的,对于其他框架项目无能为力,老项目改造也比较耗费成本,规划好的新项目可以考虑。

方案4:如mybatis,jpa都有自带的拦截器可以对数据进行处理

优点:

1.相对灵活,可以自定义实现功能,具体实现逻辑可自行查找官方文档。

2.只关注自己的连接方式,经过了多个版本迭代相对稳定。

3.对于数据库版本没有区别,基于框架层面处理。

缺点:

1.存在一定的局限性,必须使用相应的框架连接才行,对于一些老系统或轻量级系统使用原生jdbc连接的无法处理。

2.不同框架都需要配置自己的拦截器,不同框架之间无法复用,拦截层面较高,如果系统中有没使用框架查询的方法无法拦截。

3.受限于框架自带的功能,对于某些特殊的需求如果框架不支持也就无能为力了。

方案5: 基于jdbc层面拦截(只是一种思路,简单研究了一个demo)

优点:

1.灵活多变,可以自行处理相应的逻辑,所有源码来源于自己写的,可扩展性强。

2.拦截层面比较靠前,直接从jdbc层面拦截,任何返回请求都可以拦截。

3.对系统的侵入性比较小,只需要在连接的url后添加一个参数就可以了,引入方式可以直接把jar包放到jdk扩展路径中去。

4.使用简单,默认可以提供相应的处理方式,可以实现0编码,如果需要自定义算法则继承拦截器重写处理方式即可。

缺点:

1.目前没有成熟的应用产品,需要根据业务自行创建。

2.适配的单位是数据库类型,每种数据库类型都需要加载不同的拦截器(当然现在是从0开始啥都没有,如果做好了一个成熟的版本使用就比较简单了)。

实现

本来最初的想法是基于aop对ResultSet接口最切面处理,但是事实不允许,因为切面必须由spring提供的对象去调用才行,而jdbc内部很多的请求丢失this.调用的没办法处理,而且不用spring就没法做切面,局限行太大了。

目前研究了方案的可行性,只研究了一种驱动,这里选择的是mysql:5.1.48版本驱动(由于mysql驱动5到6有一个大的改动从com.mysql.jdbc.Driver切换到了com.mysql.cj.jdbc.Driver)导致了很多包路径改动了,6版本的驱动可能不一致暂未测试。

看了下目录结构,打开mysql驱动包里面有一个路径:com.mysql.jdbc.interceptors

请添加图片描述

ResultSetScannerInterceptor这个拦截器很明显就是拦截返回内容的,找到入口,后面再使用他就很简单了,自定义一个相应的拦截器在连接的url后面加上参数statementInterceptors=com.yanci.MaskResultSetScannerInterceptor指定使用这个拦截器即可。

说明

这一部分代码不多,主要是提供一种思路一起看一下运行效果和可部署方式,内置处理方式暂时比较简单,后续可以梳理一套常见的敏感字段及处理算法,后续再研究,期望的是数据库中存明文,当查询的时候如果字段为password则对返回的结果做加密处理(可解密,主要防止打印对象的时候日志暴露),如果返回的是phone则对中间部分内容做脱敏处理,具体的根据登录用户做数据权限可以继承这个类里面写session判断逻辑等,见效果图。
请添加图片描述

注意

由于jpa的查询字段可能会由别名如phone可能变成 phone5_0_,为了兼容jpa放弃精确匹配字段的方法

源码如下:

package com.yanci;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.ResultSetInternalMethods;
import com.mysql.jdbc.Statement;
import com.mysql.jdbc.StatementInterceptor;
import com.yanci.util.DesUtil;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.Properties;

/**
 * @description: 自定义数据返回结果拦截器
 * @author: Yanci
 * @date: 2022/6/16 13:13
 */
public class MaskResultSetScannerInterceptor implements StatementInterceptor {
    public MaskResultSetScannerInterceptor() {
    }

    @Override
    public void init(Connection conn, Properties props) throws SQLException {

    }

    @Override
    public ResultSetInternalMethods postProcess(String sql, Statement interceptedStatement, final ResultSetInternalMethods originalResultSet, Connection connection) throws SQLException {

        return (ResultSetInternalMethods) Proxy.newProxyInstance(originalResultSet.getClass().getClassLoader(), new Class[]{ResultSetInternalMethods.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if ("equals".equals(method.getName())) {
                    return args[0].equals(this);
                } else {
                    Object invocationResult = method.invoke(originalResultSet, args);
                    String methodName = method.getName();
                    if (invocationResult != null && invocationResult instanceof String && "getString".equals(methodName)) {
                        return dealString(sql, invocationResult, args);
                    }
                    return invocationResult;
                }
            }
        });
    }

    @Override
    public boolean executeTopLevelOnly() {
        return false;
    }

    @Override
    public void destroy() {

    }

    @Override
    public ResultSetInternalMethods preProcess(String sql, Statement interceptedStatement, Connection connection) throws SQLException {
        return null;
    }

    /**
     * 自定义算法,可以自行根据算法实现
     *
     * @param sql 这里预留sql参数最好在子类中重写这个方法,自行实现这个处理数据的逻辑就行
     * @param obj
     * @param org
     * @return
     */
    public Object dealString(String sql, Object obj, Object... org) {
        if (obj == null || org == null || org.length == 0) {
            return obj;
        }
//        if (Objects.equals(org[0], "phone")) {
//           在jpa中传过来的phone可能变成 phone5_0_,为了兼容jpa放弃精确匹配字段的方法
        String key = String.valueOf(org[0]);
        String value = String.valueOf(obj);

        if (key.contains("phone")) {
            char[] chars = value.toCharArray();
            int length = chars.length / 3;
            for (int i = length; i < 2 * length; i++) {
                chars[i] = '*';
            }
            return new String(chars);
        } else if (key.contains("password")) {
            return DesUtil.encrypt(value);
        }
        return obj;
    }
}

另外在提供一个内置加密类可以根据动态密钥进行加解密,对于返回敏感字段实现脱敏,返回的密码等不可见字段实现加密功能

DesUtil

package com.yanci.util;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Objects;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * DES加密 解密算法
 *
 * @author Yanci
 * @date 2022/6/18  11:45
 * Description
 */

public class DesUtil {

    private final static String DES = "DES";
    private final static String ENCODE = "GBK";
    private static String defaultKey = "";
    private static Object obj = new Object();

    public static void checkEncryKey() {
        if (Objects.equals("", defaultKey)) {
            synchronized (obj) {
                if (Objects.equals("", defaultKey)) {
                    //密钥最少8位,且只有前八位有效,所以这里前面拼接8位长度字符串避免报错
                    defaultKey = ResourcesUtil.getProperties("key") + "abcdefgh";
                }
            }
        }
    }

    /**
     * 使用 默认key 加密
     *
     * @param data 待加密数据
     * @return
     * @throws Exception
     */
    public static String encrypt(String data) {
        checkEncryKey();
        try {
            byte[] bt = encrypt(data.getBytes(ENCODE), defaultKey.getBytes(ENCODE));
            String strs = new BASE64Encoder().encode(bt);
            return strs;
        } catch (Exception e) {
        }
        return data;
    }

    /**
     * 使用 默认key 解密
     *
     * @param data 待解密数据
     * @return
     * @throws IOException
     * @throws Exception
     */
    public static String decrypt(String data) throws IOException, Exception {
        checkEncryKey();
        if (data == null)
            return null;
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] buf = decoder.decodeBuffer(data);
        byte[] bt = decrypt(buf, defaultKey.getBytes(ENCODE));
        return new String(bt, ENCODE);
    }

    /**
     * Description 根据键值进行加密
     *
     * @param data 待加密数据
     * @param key  密钥
     * @return
     * @throws Exception
     */
    public static String encrypt(String data, String key) throws Exception {
        byte[] bt = encrypt(data.getBytes(ENCODE), key.getBytes(ENCODE));
        String strs = new BASE64Encoder().encode(bt);
        return strs;
    }

    /**
     * 根据键值进行解密
     *
     * @param data 待解密数据
     * @param key  密钥
     * @return
     * @throws IOException
     * @throws Exception
     */
    public static String decrypt(String data, String key) throws IOException,
            Exception {
        if (data == null)
            return null;
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] buf = decoder.decodeBuffer(data);
        byte[] bt = decrypt(buf, key.getBytes(ENCODE));
        return new String(bt, ENCODE);
    }

    /**
     * Description 根据键值进行加密
     *
     * @param data
     * @param key  加密键byte数组
     * @return
     * @throws Exception
     */
    private static byte[] encrypt(byte[] data, byte[] key) throws Exception {
        // 生成一个可信任的随机数源
        SecureRandom sr = new SecureRandom();

        // 从原始密钥数据创建DESKeySpec对象
        DESKeySpec dks = new DESKeySpec(key);

        // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
        SecretKey securekey = keyFactory.generateSecret(dks);

        // Cipher对象实际完成加密操作
        Cipher cipher = Cipher.getInstance(DES);

        // 用密钥初始化Cipher对象
        cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);

        return cipher.doFinal(data);
    }

    /**
     * Description 根据键值进行解密
     *
     * @param data
     * @param key  加密键byte数组
     * @return
     * @throws Exception
     */
    private static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        // 生成一个可信任的随机数源
        SecureRandom sr = new SecureRandom();

        // 从原始密钥数据创建DESKeySpec对象
        DESKeySpec dks = new DESKeySpec(key);

        // 创建一个密钥工厂,然后用它把DESKeySpec转换成SecretKey对象
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
        SecretKey securekey = keyFactory.generateSecret(dks);

        // Cipher对象实际完成解密操作
        Cipher cipher = Cipher.getInstance(DES);

        // 用密钥初始化Cipher对象
        cipher.init(Cipher.DECRYPT_MODE, securekey, sr);

        return cipher.doFinal(data);
    }


    public static void main(String[] args) throws Exception {

        String data = "12345789";
        System.err.println(encrypt(data, "23456789"));
        System.err.println(encrypt(data, "1234578910"));
        System.err.println(encrypt(data, "12345789111"));
        System.err.println(encrypt(data, "1234578910asd"));

    }

}

加载动态密钥:ResourcesUtil

package com.yanci.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @author Yanci
 * @date 2022/6/18  12:01
 * Description
 */
public class ResourcesUtil {
    public static String getProperties(String name) {
        Properties props = getPropertiesInCache();
        String value = props.getProperty(name);
        return value;
    }

    public static Properties getPropertiesInCache() throws SecurityException {
        Properties pro = null;
        if (pro == null) {
            pro = getProperties();
        }
        return pro;
    }

    private static Properties getProperties() {
        try (InputStream in = Thread.currentThread().getContextClassLoader().getResource("encry.properties").openStream()) {
            Properties props = new Properties();
            props.load(in);
            in.close(); // 别忘了关流
            return props;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}


代码结构图如下
请添加图片描述

使用方式:

1.连接方式添加参数

请添加图片描述

2.引入方式

2-1.引入maven坐标

请添加图片描述

2-2.直接把jar包加载到jdk和jre的扩展路径下
请添加图片描述

可以看到也是可运行的,同时不会影响其他工程的使用,这样就不需要去改动所有工程的pom文件了。

请添加图片描述

应用框架测试

1.原生jdbcUtil连接

请添加图片描述

2.mybatis框架测试

请添加图片描述

3.jpa+自定义拦截器

package com.yanci.intercepter;

import com.yanci.MaskResultSetScannerInterceptor;
import com.yanci.util.DesUtil;

import java.util.Objects;

/**
 * @author Yanci
 * @date 2022/6/18  13:51
 * Description
 */
public class CustomerMaskResultSetScannerInterceptor extends MaskResultSetScannerInterceptor {
    /**![QQ截图20220618195116](D:\Users\yx\Desktop\images\QQ截图20220618195116.png)
     * 自定义的实现类可以根据自己业务处理,根据session,
     * 当前登录用户id的相关信息等内容,比如超级用户admin可以查看所有数据,
     * 当然这个属于具体业务,精确到这个界别也完全可以用框架自带的拦截器处理了,只是预留这么一个方法,实际比较鸡肋
     *
     * @param sql
     * @param obj
     * @param org
     * @return
     */
    @Override
    public Object dealString(String sql, Object obj, Object... org) {
        if (obj == null || org == null || org.length == 0) {
            return obj;
        }
        String key = String.valueOf(org[0]);
        String value = String.valueOf(obj);
        //解析sql判断来源的字段是哪一张表
        String tableName = getTableName(sql, key);

        if (Objects.equals(getUserId(), "1")) {
            //如果不是user表不处理
            return obj;
        }
        if (!Objects.equals(tableName, "user")) {
            //如果不是user表不处理
            return obj;
        }
        if (key.contains("phone")) {
            char[] chars = value.toCharArray();
            int length = chars.length / 3;
            for (int i = length; i < 2 * length; i++) {
                chars[i] = '#';
            }
            return new String(chars);
        } else if (key.contains("password")) {
            return DesUtil.encrypt(value);
        }
        return obj;
    }

    private String getUserId() {
        return "user";
    }

    private String getTableName(String sql, String key) {
        //解析sql的语句进行处理,暂时写死返回user表

        return "user";
    }

}

连接地址修改,指向自定义的拦截器
请添加图片描述

效果,原有的*替换成了#

请添加图片描述
github源码地址

总结

总结一下以上五种方案1、2、3几乎可以忽略,不在考虑的范围内,目前比常用的成熟方案还是用注解+框架自带拦截器的能解决大部分问题,因为现在已经很少由大的系统还用原生的jdbc去连接了,第五种新方案属于突发奇想想研究一下源码,尝试了一下确实可用,当作学习的一个方向还是不错的,目前只研究了mysql的驱动实现方式,其他数据库的驱动需要再研究,在特定场景确实是可以给用户带来价值的,这个方案也从侧面反映了做jdbc驱动的开发人员想的还是比较周到的,有可能需要用的内容都给我们留下了相应的处理方式,只是在我们日常使用中没有去过深地研究他每一个参数存在的意义,欢迎有兴趣的小伙伴们一起研究。

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-06-20 23:03:25  更:2022-06-20 23:05:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:31:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码