1►
前言
洞态 IAST 团队开发项目API导航功能。本文以SpringBoot项目为例,对其进行所有API接口的扫描。
2►
环境
jdk11
SpringBoot2.x
3►
原理
1.找到SpringBoot所有接口的思路
了解 SpringMVC 的同学都知道,Spring 框架把接口写在 Controller 层,获取每一个 Controller 中的接口理论上可以获取项目的全部接口。因为 Spring 独特的 Ioc 机制,所有的接口和 service 都存在一个 map 容器内,我们只要找到这个 map 容器即可。
RequestMappingHandlerMapping类是在DispatcherServlet的初始化过程中自动加载的,默认会自动加载所有实现HandlerMapping接口的bean。
所以在 DispatcherServlet 初始化结束后获取到 RequestMappingHandlerMapping 对象,我们所需要的 map 就在这个对象中。
RequestMappingHandlerMapping 的初始化过程:
RequestMappingHandlerMapping 会遍历所有 bean,如果 bean 实现带有注解 @Controller 或者 @RequestMapping 则进一步调用 detectHandlerMethods 处理,处理逻辑大致就是根据 @RequestMapping 配置的信息,构建 RequestMappingInfo,然后注册到 MappingRegistry 中。
2.实现通过 RequestMappingHandlerMapping 获取 SpringBoot 所有接口
要实现通过 RequestMappingHandlerMapping 获取SpringBoot所有接口,首先要获取到 RequestMappingHandlerMapping 这个 Bean。获取Bean通过 ApplicationContext 对象的getBean()方法。
获取 ApplicationContext 对象的方式有很多,洞态 IAST 使用字节码插桩,所以只需要找到 ApplicationContext 初始化完成的源码位置即可。获取到 ApplicationContext 对象之后就能得到 RequestMappingHandlerMapping 对象,该对象的getHandlerMethods()方法即可获取到所有的接口了。
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> methodMap = mapping.getHandlerMethods();
3.字节码插桩
DongTai-agent-java 使用 ASM 框架进行字节码插桩,ASM 是一个 Java 字节码操控框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
DongTai-agent-java 会对大部分类打标签,将不同标签的类进行不同的字节码插桩。上文中已经描述过我们需求的类,就是有 ApplicationContext 对象的类,只要获取到 ApplicationContext 对象,我们就可以取到我们想要的 API 接口。
FrameworkServlet是Spring web框架的基本 servlet 实现类,通过 JavaBean 的方式集成了ApplicationContext,使用该类的好处是可以确保ApplicationContext已经加载完毕,可以获取到完整的API信息,而且触发该类的方式很简单,当应用启动之后访问项目就可以出发到该类,这时就可以向Server端发送报告。
4.向Server端发送报告
新建报告模型 ApiDataModel,将 Server 端所需的信息封装一个模型。
public class ApiDataModel {
private String url;
private String method;
private String clazz;
private Map<String,String>[] parameters;
private String returnType;
private String file;
private String controller;
}
生成 json 报告
private static String createReport(List<ApiDataModel> apiList) {
JSONObject report = new JSONObject();
JSONObject detail = new JSONObject();
JSONArray apiData = new JSONArray();
report.put(ReportConstant.REPORT_KEY, ReportConstant.REPORT_API);
report.put(ReportConstant.REPORT_VALUE_KEY, detail);
detail.put(ReportConstant.AGENT_ID,AgentRegisterReport.getAgentFlag());
detail.put(ReportConstant.API_DATA, apiData);
for (ApiDataModel apiDataModel:apiList
) {
JSONObject api = new JSONObject();
apiData.put(api);
api.put(ReportConstant.API_DATA_URI,apiDataModel.getUrl());
api.put(ReportConstant.API_DATA_METHOD,apiDataModel.getMethod());
api.put(ReportConstant.API_DATA_CLASS,apiDataModel.getClazz());
List<Map<String, String>> parameters = apiDataModel.getParameters();
JSONArray parametersJson = new JSONArray();
api.put(ReportConstant.API_DATA_PARAMETERS,parametersJson);
for (Map<String,String> parameter:parameters
) {
JSONObject parameterjson = new JSONObject();
parametersJson.put(parameterjson);
parameterjson.put(ReportConstant.API_DATA_PARAMETER_NAME,parameter.get(ReportConstant.API_DATA_PARAMETER_NAME));
parameterjson.put(ReportConstant.API_DATA_PARAMETER_TYPE,parameter.get(ReportConstant.API_DATA_PARAMETER_TYPE));
parameterjson.put(ReportConstant.API_DATA_PARAMETER_ANNOTATION,parameter.get(ReportConstant.API_DATA_PARAMETER_ANNOTATION));
}
api.put(ReportConstant.API_DATA_RETURN,apiDataModel.getReturnType());
api.put(ReportConstant.API_DATA_FILE,apiDataModel.getFile());
api.put(ReportConstant.API_DATA_CONTROLLER,apiDataModel.getController());
}
return report.toString();
}
向 Server 端发送报告
public static void sendApi(MethodEvent event, AtomicInteger invokeIdSequencer) {
if (!isSend) {
Object applicationContext = event.returnValue;
createClassLoader(applicationContext);
loadApplicationContext();
String invoke = null;
try {
invoke = (String) getAPI.invoke(null, applicationContext);
EngineManager.sendNewReport(invoke);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e){
e.printStackTrace();
}
isSend = true;
}
}
4►
总结
Spring 框架对 Javaweb 项目的管理使得获取 Spring 项目的所有 API 比较容易实现,但是对于没有使用框架原生的 Javaweb 项目获取API就比较困难,各位师傅如果有好的方法可以在评论区留言或者私信联系。
火线安全
原文始发于微信公众号(火线安全平台):洞态IAST对SpringBoot项目扫描API接口的实现方法
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论