在上篇文章中由于字数限制,没有将本文内容加进去,所以这里单独写一篇关于Java Agnet实际应用的文章。
如果你看完了上篇文章:关于Java Agent的使用、工作原理、及hotspot源码 解析,那么此篇的应用文章就相当轻松了 当然你需要使用过 mybatis plus 这个框架(因为我们本文是对这个框架的代码进行插桩) 不过我想干Java的这个(mybatis plus)应该是基本功,对这个框架就不多介绍了。
1、我们的目的:(写时加密,读时解密)
因为我这里使用baomidou(mybatis plus)开发的,所以直接找到这个方法(注意我可不是凭想象来的而是经过了对增删改查这几个方法的debug 最终发现baomidou的mybatis plus最终都会走com.baomidou.mybatisplus.core.override
类的execute
方法,如下:) 所以说需要插桩的地方我们就找到了。接下来开干!
2、编写Java Agent
2.1、编写premain方法并实现ClassFileTransformer的transform类,以便类加载时回调到transform 从而对指定的类中的方法进行增强即 插桩!
java
package com.xzll.agent.config;
import com.xzll.agent.config.advice.MysqlFieldEncryptAndDecryptAdvice;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @Author: hzz
* @Date: 2023/3/3 09:15:21
* @Description: MYSQL加解密 agent
*/
public class MysqlFieldCryptByExecuteBodyAgent {
/**
* 实现了带Instrumentation参数的premain()方法。
*/
public static void premain(String args, Instrumentation inst) throws Exception {
//调用addTransformer()方法对启动时所有的类(应用层)进行拦截
inst.addTransformer(new MysqlReadWriteTransformer(), true);
}
static class MysqlReadWriteTransformer implements ClassFileTransformer {
/**
* 如果事先知道哪些类需要修改,最简单的修改类方式如下:
* <p>
* 1、通过调用ClassPool.get()方法获取一个CtClass对象
* 2、修改它
* 3、调用CtClass对象的writeFile()或toBytecode()方法获取修改后的类文件
**/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//拦截指定要插桩 & 增强的类
if ("com/baomidou/mybatisplus/core/override/MybatisMapperMethod".equals(className)) {
CtClass clazz = null;
System.out.println("对MybatisMapperMethod执行插桩实现读解密,写加密。");
try {
// 从ClassPool获得CtClass对象 (ClassPool对象是CtClass对象的容器,CtClass对象是类文件的抽象表示)
final ClassPool classPool = ClassPool.getDefault();
//这一步必不可少 和类加载器有关系,且maven中要配置 addClasspath=true
//不加的话插桩时找不到MysqlFieldEncryptAndDecryptAdvice这个类
classPool.insertClassPath(new ClassClassPath(MysqlFieldEncryptAndDecryptAdvice.class));
clazz = classPool.get("com.baomidou.mybatisplus.core.override.MybatisMapperMethod");
CtMethod getTime = clazz.getDeclaredMethod("execute");
String body = "{\n" +
"return com.xzll.agent.config.advice.MysqlFieldEncryptAndDecryptAdvice.executeAgent($0,$1,$2);\n" +
"}\n";
getTime.setBody(body);
//通过CtClass的toBytecode(); 方法来获取 被修改后的字节码
return clazz.toBytecode();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (null != clazz) {
//调用CtClass对象的detach()方法后,对应class的其他方法将不能被调用。但是,你能够通过ClassPool的get()方法,
//重新创建一个代表对应类的CtClass对象。如果调用ClassPool的get()方法, ClassPool将重新读取一个类文件,并且重新创建一个CtClass对象,并通过get()方法返回
//如下所说:
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
}
System.out.println("对sqlSession插桩完成");
}
}
return classfileBuffer;
}
}
}
2.2、对指定方法插桩与增强
java
package com.xzll.agent.config.advice;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.override.MybatisMapperMethod;
import com.xzll.agent.config.util.AESUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* @Author: 黄壮壮
* @Date: 2024/4/20 10:37:11
* @Description:
*
* 对baomidou 的 com.baomidou.mybatisplus.core.override.MybatisMapperMethod 类中的 execute方法进行增强,
* 写时拦截到需要加密的字段(DO类上带有注解 @SensitiveData且字段上带有注解@EncryptTransaction的实体类中的字段 ) 进行aes加密,读时拦截需要解密(被@EncryptTransaction修饰)的字段用aes工具解密
*/
public class MysqlFieldEncryptAndDecryptAdvice {
/**
* 解密字段(如果类和其中的字段 存在被敏感注解修饰的话)
*
* @param args
*/
private static <T> T decryptRead(T resultObject) {
try {
if (Objects.nonNull(resultObject)) {
if (resultObject instanceof ArrayList) {
List resultList = (List) resultObject;
if (!CollectionUtils.isEmpty(resultList) && existSensitiveData(resultList.get(0))) {
for (Object result : resultList) {
decrypt(result);
}
}
} else {
if (existSensitiveData(resultObject)) {
decrypt(resultObject);
}
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
return resultObject;
}
public static <T> T decrypt(T result) {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被DecryptTransaction注解的字段 将其解密
if (Objects.nonNull(field.getAnnotation(DecryptTransaction.class))) {
field.setAccessible(true);
try {
Object object = field.get(result);
String value = (String) object;
if (StringUtils.isNotBlank(value)) {
//对注解的字段进行逐一解密
try {
value = AESUtils.decrypt(value);
} catch (Exception e) {
}
field.set(result, value);
}
} catch (Exception e) {
}
}
}
return result;
}
public static <T> T encrypt(T result) {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被DecryptTransaction注解的字段 将其解密
if (Objects.nonNull(field.getAnnotation(EncryptTransaction.class))) {
field.setAccessible(true);
try {
Object object = field.get(result);
String value = (String) object;
if (StringUtils.isNotBlank(value)) {
//对注解的字段进行逐一解密
try {
value = AESUtils.encrypt(value);
} catch (Exception e) {
}
field.set(result, value);
}
} catch (Exception e) {
}
}
}
return result;
}
/**
* 加密写字段(如果类和其中的字段 存在被敏感注解修饰的话)
*
* @param args
*/
public static void encryptWrite(Object[] args) {
try {
for (Object object : args) {
if (object instanceof List) {
List resultList = (List) object;
if (!CollectionUtils.isEmpty(resultList) && existSensitiveData(resultList.get(0))) {
for (Object result : resultList) {
encrypt(result);
}
}
} else {
if (existSensitiveData(object)) {
encrypt(object);
}
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* 将原 MybatisMapperMethod 类的 execute方法进行增强
*
* @param mapperMethod
* @param sqlSession
* @param args
* @return
*/
public static Object executeAgent(MybatisMapperMethod mapperMethod, SqlSession sqlSession, Object[] args) {
Object result;
Object param;
/**
* 由于源代码中,都是直接使用this来访问 成员变量/方法,这种方式在 MybatisMapperMethod 类肯定是可行的,
* 但是不在此类时通过this无法访问,所以就有了下边的:使用反射 获取私有成员变量/执行成员方法 )
*/
//(使用反射获取 MybatisMapperMethod 类中的私有成员变量)
MapperMethod.SqlCommand commandValue = (MapperMethod.SqlCommand) ReflectUtil.getFieldValue(mapperMethod, "command");
MapperMethod.MethodSignature methodValue = (MapperMethod.MethodSignature) ReflectUtil.getFieldValue(mapperMethod, "method");
switch (commandValue.getType()) {
case INSERT:
//原代码
//param = this.method.convertArgsToSqlCommandParam(args);
//result = this.rowCountResult(sqlSession.insert(commandValue.getName(), param));
//改后代码
MysqlFieldEncryptAndDecryptAdvice.encryptWrite(args);
param = methodValue.convertArgsToSqlCommandParam(args);
result = ReflectUtil.invoke(mapperMethod, "rowCountResult", sqlSession.insert(commandValue.getName(), param));
break;
case UPDATE:
//param = this.method.convertArgsToSqlCommandParam(args);
//result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
MysqlFieldEncryptAndDecryptAdvice.encryptWrite(args);
param = methodValue.convertArgsToSqlCommandParam(args);
result = ReflectUtil.invoke(mapperMethod, "rowCountResult", sqlSession.update(commandValue.getName(), param));
break;
case DELETE:
//param = this.method.convertArgsToSqlCommandParam(args);
//result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
param = methodValue.convertArgsToSqlCommandParam(args);
result = ReflectUtil.invoke(mapperMethod, "rowCountResult", sqlSession.delete(commandValue.getName(), param));
break;
case SELECT:
if (methodValue.returnsVoid() && methodValue.hasResultHandler()) {
//this.executeWithResultHandler(sqlSession, args);
ReflectUtil.invoke(mapperMethod, "executeWithResultHandler", sqlSession, args);
result = null;
} else if (methodValue.returnsMany()) {
//result = this.executeForMany(sqlSession, args);
result = ReflectUtil.invoke(mapperMethod, "executeForMany", sqlSession, args);
} else if (methodValue.returnsMap()) {
//result = this.executeForMap(sqlSession, args);
result = ReflectUtil.invoke(mapperMethod, "executeForMap", sqlSession, args);
} else if (methodValue.returnsCursor()) {
//result = this.executeForCursor(sqlSession, args);
result = ReflectUtil.invoke(mapperMethod, "executeForCursor", sqlSession, args);
} else if (IPage.class.isAssignableFrom(methodValue.getReturnType())) {
//result = this.executeForIPage(sqlSession, args);
result = ReflectUtil.invoke(mapperMethod, "executeForIPage", sqlSession, args);
} else {
param = methodValue.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(commandValue.getName(), param);
if (methodValue.returnsOptional() && (result == null || !methodValue.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + commandValue.getName());
}
if (result == null && methodValue.getReturnType().isPrimitive() && !methodValue.returnsVoid()) {
throw new BindingException("Mapper method '" + commandValue.getName() + " attempted to return null from a method with a primitive return type (" + methodValue.getReturnType() + ").");
} else {
if (Objects.equals(commandValue.getType(), SqlCommandType.SELECT) && result != null) {
result = MysqlFieldEncryptAndDecryptAdvice.decryptRead(result);
}
return result;
}
}
/**
* 是否存在敏感字段 true存在 false不存在
*
* @param object
* @return
*/
private static boolean existSensitiveData(Object object) {
Class<?> objectClass = object.getClass();
SensitiveData sensitiveData = AnnotationUtil.getAnnotation(objectClass, SensitiveData.class);
return Objects.nonNull(sensitiveData);
}
}
2.3、指定哪些字段需要加/解密(编写DO类,指定加/解密的字段)
以下是DO实体定义: 一个字段想被修饰首先此字段所在的类需要被 @SensitiveData
注解修饰,然后再加上 @EncryptTransaction(加密)或 @DecryptTransaction(解密)
注解,@SensitiveData存在的意义是防止无效的遍历,提前将不带此注解的排除,提高性能。
2.4、指定premain类和打agent jar包
2.5、开发增删改查接口
controller层 service层
2.6、启动springboot服务,并在启动时添加vm参数: -javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
2.7、来吧展示
单个添加(insert):
debug: db数据:
批量添加 (batchInsert):
注意我的批量插入和下边的批量更新都是使用
foreach
标签来实现的:
发起批量插入请求: debug: db:
批量更新(batchUpdateUser
):
debug: db:
查询 (selectList):
debug: 查询出来的效果:
还有个单个更新和删除我们就不试了总之只要对对应的方法增强了,就都能达到此效果。
3、温馨提示
但是这种方式一般只适合管理端的查询或写入,面向app的 高频 查询或写入时 使用该方式要评估考虑性能问题最好是能压测从而来评估是否合适。
到此为止,Java Agent就告一段落接下来向其他技术发起进攻!骑兵连!进攻!!!😂😂😂