这是我参与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