[toc]
# 0x01 前言
熟悉一下 Tomcat 框架
# 0x02 Java web 三大件
Servlet,Filter,Listener
当 Tomcat 接收到请求时候,依次会经过 Listener -> Filter -> Servlet
# Servlet
# 概念
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。
# 请求流程
客户端发起一个 http 请求,比如 get 类型。
Servlet 容器接收到请求,根据请求信息,封装成 HttpServletRequest 和 HttpServletResponse 对象。这步也就是我们的传参。
Servlet 容器调用 HttpServlet 的 init () 方法,init 方法只在第一次请求的时候被调用。
Servlet 容器调用 service () 方法。
service () 方法根据请求类型,这里是 get 类型,分别调用 doGet 或者 doPost 方法,这里调用 doGet 方法。
doXXX 方法中是我们自己写的业务逻辑。
业务逻辑处理完成之后,返回给 Servlet 容器,然后容器将结果返回给客户端。
容器关闭时候,会调用 destory 方法。
1 | import javax.servlet.*; |
# 生命周期
1)服务器启动时 (web.xml 中配置 load-on-startup=1,默认为 0) 或者第一次请求该 servlet 时,就会初始化一个 Servlet 对象,也就是会执行初始化方法 init (ServletConfig conf)。
2)servlet 对象去处理所有客户端请求,在 service (ServletRequest req,ServletResponse res) 方法中执行
3)服务器关闭时,销毁这个 servlet 对象,执行 destroy () 方法。
4)由 JVM 进行垃圾回收。
# Filter
# 概念
filter 也称之为过滤器,是对 Servlet 技术的一个强补充,其主要功能是在 HttpServletRequest 到达 Servlet 之前,拦截客户的 HttpServletRequest ,根据需要检查 HttpServletRequest,也可以修改 HttpServletRequest 头和数据;在 HttpServletResponse 到达客户端之前,拦截 HttpServletResponse ,根据需要检查 HttpServletResponse,也可以修改 HttpServletResponse 头和数据。
这里如果我们自己创建一个 filter,放在正常流程的 filter 之前,并且将恶意代码注册进去,那就可以达到命令执行效果,也就成为了一个内存 webshell
# 基本工作原理
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
2、当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
4、但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。
5、只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
6、如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
也就是说
Filter 中的 Filter 访问需要在 web.xml 里面定义路径
Filter 有一条 FilterChain,也就是由多个 Filter 组成的,会进行一个个的 Filter 操作,最后一个 Filter 最后会执行 Servlet.service ()
# 生命周期
与 servlet 一样,Filter 的创建和销毁也由 Web 容器负责。Web 应用程序启动时,Web 服务器将创建 Filter 的实例对象,并调用其 init () 方法,读取 web.xml 配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter 对象只会创建一次,init 方法也只会执行一次)。开发人员通过 init 方法的参数,可获得代表当前 filter 配置信息的 FilterConfig 对象。 Filter 对象创建后会驻留在内存,当 Web 应用移除或服务器停止时才销毁。在 Web 容器卸载 Filter 对象之前被调用。该方法在 Filter 的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
1 | import javax.servlet.*; |
# Filter 链
当多个 Filter 同时存在的时候,组成了 Filter 链。Web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter。当第一个 Filter 的 doFilter 方法被调用时,web 服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法,通过判断 FilterChain 中是否还有 Filter 决定后面是否还调用 Filter。
# Listener
# 概念
Java Web 开发中的监听器(Listener)就是 Application、Session 和 Request 三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
ServletContextListener:对 Servlet 上下文的创建和销毁进行监听; ServletContextAttributeListener:监听 Servlet 上下文属性的添加、删除和替换;
HttpSessionListener:对 Session 的创建和销毁进行监听。Session 的销毁有两种情况,一个中 Session 超时,还有一种是通过调用 Session 对象的 invalidate () 方法使 session 失效。
HttpSessionAttributeListener:对 Session 对象中属性的添加、删除和替换进行监听;ServletRequestListener:对请求对象的初始化和销毁进行监听; ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
# 0x03 Tomcat 基础介绍
Apache 是 Web 服务器(静态解析,如 HTML),Tomcat 是 java 应用服务器(动态解析,如 JSP)
Tomcat 只是一个 servlet (jsp 也翻译成 servlet) 容器,可以认为是 Apache 的扩展,但是可以独立于 Apache 运行。
# 0x04 Tomcat 架构
主要有 server、service、connector、container 四个部分
图中可以看出 Tomcat 的心脏是两个组件:Connector 和 Container:
Connector 主要负责对外交流,进行 Socket 通信 (基于 TCP/IP),解析 HTTP 报文,对应下图中的 http 服务器;
Container 主要处理 Connector 接受的请求,主要是处理内部事务,加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求,对应下图中的 servlet 容器。
# Server
即服务器,代表整个 Tomcat 服务器,它要能够提供一个接口让其它程序能够访问到这个 Service 集合、同时要维护它所包含的所有 Service 的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的 Service。还有其它的一些次要的任务
# Service
Service 主要是为了关联 Connector 和 Container,同时会初始化它下面的其它组件,在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。
Tomcat 中 Service 接口的标准实现类是 StandardService ,它不仅实现了 Service 借口同时还实现了 Lifecycle 接口,这样它就可以控制它下面的组件的生命周期了
# Connecter
Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。
socket 通信
解析处理应用层协议,如将 socket 连接封装成 request 和 response 对象,后续交给 Container 来处理
将 Request 转换为 ServletRequest,将 Response 转换为 ServletResponse
其中 Tomcat 设计了三个组件,其负责功能如下:
- EndPoint: 负责网络通信,将字节流传递给 Processor;
- Processor: 负责处理字节流生成 Tomcat Request 对象,将 Tomcat Request 对象传递给 Adapter;
- Adapter: 负责将 Tomcat Request 对象转化成 ServletRequest 对象,传递给容器。
# Adapter 组件
由于协议的不同,Tomcat 定义了自己的 Request 类来存放请求信息,但是这个不是标准的 ServletRequest。于是需要使用 Adapter 将 Tomcat Request 对象转成 ServletRequest 对象,然后就能调用容器的 service 方法。
简而言之,Endpoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池进行处理,SocketProcessor 的 run 方法将调用 Processor 组件进行应用层协议的解析,Processor 解析后生成 Tomcat Request 对象,然后会调用 Adapter 的 Service 方法,方法内部通过如下代码将 Request 请求传递到容器中。
1 | connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); |
# Container
Container(又名 Catalina)用于处理 Connector 发过来的 servlet 连接请求,它是容器的父接口,所有子容器都必须实现这个接口,Container 容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是:Engine、Host、Context、Wrapper,这四个组件不是平行的,而是父子关系,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。
- Engine: 最顶层容器组件,可以包含多个 Host。实现类为
org.apache.catalina.core.StandardEngine
- Host: 代表一个虚拟主机,每个虚拟主机和某个域名 Domain Name 相匹配,可以包含多个 Context。实现类为
org.apache.catalina.core.StandardHost
- Context: 一个 Context 对应于一个 Web 应用,可以包含多个 Wrapper。实现类为
org.apache.catalina.core.StandardContext
- Wrapper: 一个 Wrapper 对应一个 Servlet。负责管理 Servlet ,包括 Servlet 的装载、初始化、执行以及资源回收。实现类为
org.apache.catalina.core.StandardWrapper
看下面这个图可能会更直观一些
通常一个 Servlet class 对应一个 Wrapper,如果有多个 Servlet 就可以定义多个 Wrapper,如果有多个 Wrapper 就要定义一个更高的 Container。
举个🌰,a.com 和 b.com 分别对应着两个 Host
每一个 Context 都有唯一的 path。这里的 path 不是指 servlet 绑定的 WebServlet 地址,而是指独立的一个 Web 应用地址。就好比 Tomat 默认的 / 地址和 /manager 地址就是两个不同的 web 应用,所以对应两个不同的 Context。要添加 Context 需要在 server.xml 中配置 docbase。
# 0x05 Tomcat 的类加载机制
由于 Tomcat 中有多个 WebApp 同时要确保之间相互隔离,所以 Tomcat 的类加载机制也不是传统的双亲委派机制。
Tomcat 自定义的类加载器 WebAppClassloader 为了确保隔离多个 WebApp 之间相互隔离,所以打破了双亲委托机制。每个 WebApp 用一个独有的 ClassLoader 实例来优先处理加载。它首先尝试自己加载某个类,如果找不到再交给父类加载器,其目的是优先加载 WEB 应用自己定义的类。
同时为了防止 WEB 应用自己的类覆盖 JRE 的核心类,在本地 WEB 应用目录下查找之前,先使用 ExtClassLoader(使用双亲委托机制)去加载,这样既打破了双亲委托,同时也能安全加载类。