使用字节码拦截JDBC结果集

1,120 阅读4分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

数据脱敏方案

一、脱敏方案调研

目的:我们要将查出的数据,根据预设的脱敏规则进行脱敏,将脱敏后的数据返回给调用者!

经过调研,业务方期待少更改或者不更改代码,进行数据脱敏和加密,现阶段,需要数据脱敏和加密的数据基本都是经过JDBC进行查询的,所以一方面考虑减少业务方的代码修改量,一方面考虑JDBC的通用性,所以我们考虑使用字节码插桩技术,再Class编译的时候,就将JDBC修改掉!

原始具体的架构图如下:

我们后续的增强业务:

二、原始JDBC的使用

我们如果是想要拦截JDBC,我们就必须要理解JDBC的查询, 假如我们有这样一个表:

CREATE TABLE `test_sql` (
  `id` int(255) NOT NULL AUTO_INCREMENT,
  `test1` varchar(255) DEFAULT NULL,
  `test2` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

JDBC查询

/**
 * 原生的JDBC测试
 *
 * @author huangfu
 * @date 2021年8月23日09:30:12
 */
public class ProtistJdbcTest {
    /**
     * 数据库的url
     */
    private static final String URL = "jdbc:mysql://10.0.10.118:3306/huangfu_0601?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false";
    /**
     * 数据库的用户名
     */
    private static final String USER_NAME = "root";
    /**
     * 数据库的密码
     */
    private static final String PASSWORD = "123456";

    public static void main(String[] args) throws Exception {
        //加载JDBC的驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //获取连接
        Connection connection = DriverManager.getConnection(URL, USER_NAME, PASSWORD);
        //验证是否存活
        boolean valid = connection.isValid(1000);
        System.out.println("连接是否有效:" + valid);
        String sql = "select * from test_sql where id=?";
        //获取预编译处理器
        PreparedStatement preparedStatement = connection.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
        //设置条件
        preparedStatement.setInt(1, 1);

        //执行查询
        ResultSet resultSet = preparedStatement.executeQuery();

        //开始查询数据
        while (resultSet.next()) {
            System.out.println("----------------------------------------------------");
            System.out.println("id:" + resultSet.getInt("id"));
            System.out.println("test1:" + resultSet.getString("test1"));
            System.out.println("test2:" + resultSet.getString("test2"));
            System.out.println("----------------------------------------------------");
        }
    }
}

我们可以看到,正常的查询基本都是通过PreparedStatement进行查询的,那么我们拦截 executeQuery方法,解析SQL,查询表字段对应的规则,根据规则,对查询到的数据(ResultSet)进行修改后返回!

三、拦截JDBC

1. 架构图例

2. 代码实例

我们以Mysql为例,假设我们有一下需求,还是以上述的SQL代码为例,我们需要将test1字段查询的数据全部都更改为 皇甫科星

我们需要一个拦截器:

/**
 * 监听器
 *
 * @author huangfu
 * @date 2021年8月23日12:04:59
 */
public class MysqlJdbcMonitor {
    @RuntimeType
    public static Object intercept(@This Object thisObject, @SuperCall Callable<?> callable) {
        Object call = null;
        try {
            call = callable.call();
            System.out.println("------------------------------拦截器打印开始--------------------------");
            System.out.println("这是拦截器打印的:" + thisObject);
            System.out.println("开始获取sql");
            Class<?> aClass = thisObject.getClass();
            Method asSql = aClass.getMethod("asSql");
            Object sql = asSql.invoke(thisObject);
            System.out.println("*******解析出来的sql为:" + sql);

            System.out.println("这是拦截器拦截的结果集:" + call);
            if (call instanceof ResultSet) {
                ResultSet resultSet = (ResultSet) call;
                freeFromImprisonment(resultSet);
                System.out.println("修改后的类型:" + resultSet.getType());


                while (resultSet.next()) {
                    System.out.println("------------------------拦截器----------------------------");
                    System.out.println("**********id:" + resultSet.getInt("id"));
                    System.out.println("**********test1:" + resultSet.getString("test1"));
                    System.out.println("**********test2:" + resultSet.getString("test2"));

                    //获取对应的字段索引
                    int waitUpdateColumnIndex = resultSet.findColumn("test1");
                    //计算对应数据索引
                    int waitUpdateColumnIndexUpdate = waitUpdateColumnIndex - 1;
                    //internalRowData
                    ReflectionUtils.doWithFields(resultSet.getClass(), field -> {
                        field.setAccessible(true);
                        Object thisRowDataObject = field.get(resultSet);
                        //开始正式读取数据
                        ReflectionUtils.doWithFields(thisRowDataObject.getClass(), internalRowDataFieldChild -> {
                            internalRowDataFieldChild.setAccessible(true);
                            //获取internalRowData的值
                            Object internalRowDataByteArray = internalRowDataFieldChild.get(thisRowDataObject);
                            if (internalRowDataByteArray != null) {
                                //获取字段的数据
                                byte[][] rowData = (byte[][]) internalRowDataByteArray;
                                //获取对应字段的数据
                                byte[] rowDatum = rowData[waitUpdateColumnIndexUpdate];
                                System.out.println("初始值:" + new String(rowDatum, StandardCharsets.UTF_8));
                                System.out.println("修改值为:皇甫科星");
                                //修改数据
                                rowData[waitUpdateColumnIndexUpdate] = "皇甫科星".getBytes(StandardCharsets.UTF_8);
                                internalRowDataFieldChild.set(thisRowDataObject, rowData);
                                System.out.println("------------------修改数据成功-------------------");
                            }
                        }, internalRowDataFieldChild -> "internalRowData".equals(internalRowDataFieldChild.getName()));
                    }, field -> "thisRow".equals(field.getName()));

                    System.out.println("-----------------------拦截器-----------------------------");
                }
                //回归对应的光标
                resultSet.absolute(0);

            }

            System.out.println("------------------------------拦截器打印结束--------------------------");

        } catch (Exception e) {
            e.printStackTrace();
        }
        return call;
    }

    /**
     * 解决ResultSet封禁问题
     *
     * @param resultSet 结果集
     */
    protected static void freeFromImprisonment(ResultSet resultSet) {
        //修改可翻滚
        ReflectionUtils.doWithFields(resultSet.getClass(), field -> {
            field.setAccessible(true);
            Object o = field.get(resultSet);
            System.out.println("原始的值为:" + o);
            field.set(resultSet, ResultSet.TYPE_SCROLL_INSENSITIVE);
        }, field -> "resultSetType".equals(field.getName()));

        //修改只读
        ReflectionUtils.doWithFields(resultSet.getClass(), field -> {
            field.setAccessible(true);
            Object o = field.get(resultSet);
            System.out.println("原始的值为:" + o);
            field.set(resultSet, ResultSet.CONCUR_UPDATABLE);
        }, field -> "resultSetConcurrency".equals(field.getName()));

    }
}

我们需要一个拦截器的主体:

/**
 * JDBC代理拦截器
 *
 * @author huangfu
 * @date 2021年8月23日11:15:10
 */
public class PinpointBootStrap {
    public static void premain(String agentArgs, Instrumentation inst) {
        //构建一个转换器
        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> builder
                // 拦截静态而且前缀为test的方法
                .method(ElementMatchers.named("executeQuery"))
                // 委托
                .intercept(MethodDelegation.to(MysqlJdbcMonitor.class));

        AgentBuilder.Listener listener = new AgentBuilder.Listener() {

            @Override
            public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {

            }

            @Override
            public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) {

            }

            @Override
            public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) {

            }

            @Override
            public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {
                throwable.printStackTrace();
            }

            @Override
            public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {

            }
        };

        new AgentBuilder
                .Default()
                // 指定需要拦截的类
                .type(ElementMatchers.named("com.mysql.cj.jdbc.ClientPreparedStatement"))
                //添加一个转换器
                .transform(transformer)
                .with(listener)
                //将上述的转换安装到javaagent的介入器
                .installOn(inst);

    }
}

我们需要准备javaagent的环境:

   <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>

        <plugin.jar.version>3.2.0</plugin.jar.version>
        <byte-buddy.version>1.11.12</byte-buddy.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>${byte-buddy.version}</version>
        </dependency>


        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>${byte-buddy.version}</version>
        </dependency>

    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.0.0</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <!--指定打包位置-->
                            <outputDirectory>C:/Users/Administrator/Desktop/asm</outputDirectory>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>com.huangfu.jdbc.PinpointBootStrap</Premain-Class>
                                        <Agent-Class>com.huangfu.jdbc.PinpointBootStrap</Agent-Class>
                                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                        <Pinpoint-Version>${project.version}</Pinpoint-Version>
                                        <Boot-Class-Path>${project.build.finalName}.jar</Boot-Class-Path>
                                    </manifestEntries>
                                </transformer>
                            </transformers>


                            <artifactSet>
                                <includes>
                                    <include>net.bytebuddy:byte-buddy</include>
                                    <include>net.bytebuddy:byte-buddy-agent</include>
                                </includes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>

                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3.尝试运行

运行命令:

java -javaagent:./bytebuddy-demo-1.0-SNAPSHOT.jar -cp ./jdbc-demo-1.0-SNAPSHOT.jar com.huangfu.jdbc.ProtistJdbcTest