TOMCAT封神之旅(四)-Session管理与安全认证

本系列文章来自《How Tomcat Works》,因本书中描述的tomcat版本是5以下的,所以后面章节在实操部分用了tomcat8的源码。

Session管理

catalina通过org.apache.catalina.Manager接口支持session管理。一个管理者总是跟一个上下文关联。其中,管理者负责创建、更新和销毁(失效)会话对象,同时返回一个有效的会话对象给任何请求组件。

一个 servlet可以使用getSession方法获得一个session对象,该方法在javax.servlet.http.HttpServletRequest定义。它在默认连接器里由org.apache.catalina.connector.HttpRequestBase类实现。

默认情况下管理器将 session 对象存储在内存中,但是 Tomcat 也允许将 session
对象存储在文件或者数据库中(通过 JDBC)。 Catalina 在org.apache.catalina.session 包中提供了 session 对象和 session 管理的相关类型。
本章使用了三节解释了 session 管理: Session、 Managers 和 Stores。最后一节介绍了一个使用上下文容器以及相关联的管理器的应用程序。

Sessions

在 servlet 编程中,一个 session 对象使用 javax.servlet.http.HttpSession
接口表示。该接口的标准实现是 StandardSession 类,该类在
org.apache.catalina.session 包中。但是出于安全的原因,管理器并不会将一
个 StandardSession 实例传递给 servlet。而是使用
org.apache.catalina.session 包中的外观类 StandardSession 类。在内部,一
个管理器使用了另一个外观: org.apache.catalina.Session 接口。

Session接口

Session接口扮演了一个Catalina内部外观的角色。标准实现的Session接口是StandardSession,它同样也实现了HttpSession接口。

由于一个 Session 对象常常被一个管理器持有,所以接口提供了 setManager 和
getManager 方法来关联一个 Session 对象和一个管理器。另外,一个 Session
实例在跟管理器相关联的容器有一个唯一的 ID。通过setId和getId方法访问Session ID。 getLastAccessedTime 方法由管理器来调用,以确定一个 Session 对象是否有效。管理器调用 setValid 方法来重置一个 session 的有效性。每当一个session实例被访问,它的访问方法都会更新它的最后访问时间。最后,管理器可以调用 expire 方法来终止一个session,使用 getSession 可以获得一个包装在该外观内的HttpSession对象。

StandardSession类

该类的构造函数接受一个管理者实例,强制一个Sesssion对象总是有一个管理者。下面几个重要的变量用于存放session状态,注意transient使得该关键字不可序列化。getSession方法通过this实例创建一个StandardSessionFacade对象。

public HttpSession getSession() {
	if (facade == null)
		facade = new StandardSessionFacade(this);
	return (facade);
}

如果Session对象超过Manager中的maxInactiveInterval变量所指的时间内没有被访问将会使session过期。通过调用Session接口的expire方法使session对象过期。

StandardSessionFacade类

传递一个session对象给servlet,Catalina可以实例化StandardSession类,填充它,然后传递它给servlet。然而,它传递一个仅仅实现HttpSession接口的StandardSessionFacade实例。这样,servlet程序不能向下转型HttpSession对象到StandardSessionFacade。

Manager

一个管理者管理会话对象。比如,创建会话对象并使它失效。一个管理者使用org.apache.catalina.Manager接口表示。在Catalina中,org.apache.catalina.session包包含了一些公共功能的实现方法的ManagerBase类。它有两个直接子类,StandardManager和PersistentManagerBase。

当运行时,StandardManager类存储session对象在内存中,然而,当停止时,它保存当时内存中的所有的session对象到文件中。当再次启动时,它从文件中重新载入session对象。

PersistentManagerBase类是管理器组件中的基类,该类在二级缓存中存储了会话对象。它有两个子类,PersistentManager和DistributedManager。

Manager接口

一个Manager接口表示一个Manager组件。一个Manager接口通过getContainer和setContainer方法将一个Manager和一个上下文联系在一起。createSession方法创建一个session对象。add方法添加一个session实例到session池,remove方法从池中移除session实例。getMaxInactiveInterval和setMaxInactiveInterval方法返回和指定Manger等待用户关联的session在销毁之前的秒数。

最后,加载和卸载方法支持持久化session到二级存储中。通过管理器实现,unload方法保存当时活动的会话到指定的存储区,load方法从存储区取回到内存。

ManagerBase类

ManagerBase类是个抽象类,所有管理者类都从该类派生。该类为它的子类提供了公共的功能。其中,ManagerBase通过createSession方法创建一个Session对象。每个Session有个唯一的ID,ManagerBase类中的protected方法generateSessionId方法就是用来返回唯一的ID。活动的session存储在一个HashMap中。

protected HashMap sessions = new HashMap();

add方法添加一个session对象到HashMap中。findSessions方法从session中返回所有活动的session。

StandardManager类

StandardManager类是管理者存储session对象到内存的标准实现。它实现了Lifecycle接口,所以能被开始和停止。stop方法的实现调用了unload方法,该方法序列化每个上下文中有效的session实例到SESSIONS.ser的文件中。该文件可以在CATALINA_HOME的工作目录可以找到。一个管理者同样负责销毁不再有效的会话对象。

在tomcat5中,StandardManager类没有实现Runnable接口。Tomcat 5中StandardManager对象中的processExpires方法由backgroundprocess方法直接调用,这在Tomcat 4中不可用。

public void backgroundProcess() {
    processExpires();
}

StandardManager中的backgroundProcess方法由org.apache.catalina.core.StandardContext实例(与此管理器关联的容器)的backgroundProcess方法调用。StandardContext定期调用其backgroundProcess方法,将在第12章中讨论。

PersistentManagerBase类

PersistentManagerBase类是所有持久管理器的父类。与StandardManager不同的是,持久管理器会存储。存储表示会托管会话对象的二级存储。PersistentManagerBase类使用一个叫store的私有对象引用。

在持久化管理器中,可以备份会话对象,也可以交换出会话对象。备份会话对象时,会将会话对象复制到存储中,原始对象保留在内存中。因此,如果服务器崩溃,则可以从存储中检索活动会话对象。当会话对象被调出时,它将被移动到存储区,因为活动会话对象的数目超过了指定的数目或者会话对象已空闲太长时间。交换的目的是节省内存。

因为一个活动的session能被交换出去,它也可以驻留在内存或在磁盘上。因此findSession(String id)方法首先从内存中查找session实例,如果没有找到,就在磁盘上查找。也并不是所有的活动session对象都会备份。PersistentManagerBase实例仅仅备份空闲时间超过maxIdleBackup的session对象。processMaxIdleBackups方法执行备份的session对象。

Stores

一个存储使用org.apache.catalina.Store接口表示,是管理者管理session持久化存储的组件。在Store接口中的两个最重要的方法是save和load。save方法保存指定的session对象到持久化存储中,load方法根据指定的session ID从存储中加载session对象。keys方法返回包含所有session ID的String数组。

StoreBase

StoreBase是抽象类,为两个子类FileStore和JDBCStore提供公共的功能。StoreBase类并没有实现Store接口的save和load方法,因为实现这些方法依赖于持久化session的类型存储。

在Tomcat5中,没有指定的线程来调用processExpires方法。而是,这个方法通过PersistentManagerBase 类相关联的backgroundProcess方法被周期性调用。

FileStore

FileStore用于将Session对象存储在文件中。该文件的名称与会话对象的标识符加上扩展名.session相同。此文件位于临时工作目录中。可以通过调用Filestore类的setDirectory来更改临时目录。

JDBCStore

存储session对象到数据库中。

Security

Realm

一般原则

认证

安装认证阀

StandardWrapper

在第5章已经说过,一共有4种类型的容器,engine,host,context和wrapper。你在前面的章节中也可以构建简单的上下文和包装器。一个上下文一般包括一个或多个包装器,每个包装器代表一个servlet。本章将会看到 Catalina 中 Wrapper 接口的标准实现。首先介绍了一个 HTTP 请求会唤醒的一系列方法,接下来介绍了javax.servlet.SingleThreadModel接口。最后介绍了StandardWrapper和StandardWrapperValve类。本章的应用程序说明了如何用 StandardWrapper 实例来表示 servlet。

单个线程模型

对于每个HTTP请求,连接器调用关联容器的invoke方法。容器然后调用它所有子类容器的invoke方法。比如,如果容器跟一个StandardContext实例相关联,那么连接器会调用StandardContext实例的invoke方法,然后该方法会调用所有它的子类容器方法(在这种情况下,子容器将会是StandardWrapper类型)。下图解释了当连接器接收到HTTP请求的过程。可以回顾下第5章中的一个容器有一个管道有一个或多个阀门。

  1. 连接器创建请求和响应对象。
  2. 连接器调用StandardContext实例的invoke方法。
  3. StandardContext类的invoke方法反过来调用上下文流水线中的invoke。StandardContext中的流水线中的基本阀门是StandardContextValve,因此,StandardContext的流水线调用StandardContextValve的invoke方法。
  4. StandardContextValve的invoke方法得到合适的包装器来对请求进行服务并调用包装器的invoke方法。
  5. StandardWrapper类是包装器的标准实现。StandardWrapper实例的invoke方法调用流水线的invoke方法。
  6. StandardWrapper类中的流水线中的基本阀门是StandardWrapperValve。因此,StandardWrapperValve类的invoke方法被调用。StandardWrapperValve类的invoke方法调用包装器的allocate方法来获得一个servlet实例。
  7. allocate方法调用load方法来加载servlet,如果servlet需要被加载的话。
  8. load方法调用servlet的init方法。
  9. StandardWrapperValve调用servlet的service方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ej0ZOcxH-1619231550087)(E:\study\tomcat\img\methodInvocation.png)]

一个servlet能实现javax.servlet.SingleThreadModel接口,一个实现这个接口的servlet被称为STM servlet。根据Servlet的规范,实现这个接口的目的是确保servlet在同一个时刻处理仅仅一个请求。引用Servlet2.4规范中的语句:

如果一个Servlet实现此接口,将保证不会有两个线程并发使用servlet的service方法。 servlet容器可以保证同步进入一个servlet的一个实例,或维持的Servlet实例池和处理每个新请求。该接口并不能避免同步而产生的问题,如访问静态类变量或该servlet以外的类或变量。

很多程序员并没有仔细阅读它,只是认为实现了SIngleThreadModel就能保证它们的servlet是线程安全的。显然并非如此,重新阅读上面的引文。

一个 servlet 实现了 SIngelThreadModel 之后确实能保证它的 service 方法不会被两个线程同时使用。为了提高 servlet 容器的性能,可以创建 STM servlet的多个实例。也就是说,STM servlet的service方法能在不同的实例中并发执行。如果servlet能访问静态类变量或其他类以外的资源时,这将会引入同步问题。

StandardWrapper

一个StandardWrapper对象的主要职责是,加载表示它的servlet并分配它的一个实例。然而,StandardWrapper类不调用servlet的service方法。这个任务留给了StandardWrapperValve对象,StandardWrapper实例的流水线中的基本阀门。StandardWrapperValve对象通过调用StandardWrapper类的allocate方法获取servlet实例。在接收到servlet实例时,StandardWrapperValve调用servlet的service方法。

当servlet第一次请求时,StandardWrapper加载servlet类。StandardWrapper实例动态加载servlet,因此需要知道servlet类的全限定名。通过StandardWrapper 类的setServletClass方法将servlet的类名传递给StandardWrapper。另外,使用setName方法也可以传递servlet名。

考虑到StandardWrapper负责在StandardWrapperValve请求的时候分配一个servlet 实例,它必须考虑一个 servlet 是否实现了 SingleThreadModel 接口 。

如果一个 servlet 没有实现 SingleThreadModel 接口, StandardWrapper 加载该
servlet 一次,对于以后的请求返回相同的实例即可。 StandardWrapper 假设
servlet 的 service 方法是现场安全的,所以并没有创建 servlet 的多个实例。
如果需要的话,由程序员自己解决资源同步问题。

对于一个 STM servlet,情况就有所不同了。 StandardWrapper 必须保证不能同
时有两个线程提交 STM servlet 的 service 方法。如果 StandardWrapper 维持一
个 STM servlet 的实例,下面是它如何调用 servlet 的 service 方法:

Servlet instance = <get an instance of the servlet>;
if ((servlet implementing SingleThreadModel>) {
    synchronized (instance) {
         instance.service(request, response);
    }
}  else {
    instance.service(request, response);
}

但是,为了性能起见,StandardWrapper维护了一个STM servlet实例池。

分配servlet

在本节开始时,StandardWrapperValve的invoke方法调用包装器的allocate方法获取一个请求servlet实例。StandardWrapper类因此必须实现该方法。allocate方法的签名如下。

public javax.servlet.Servlet allocate() throws ServletException;

注意,allocate方法返回的是请求servlet的一个实例。必要的支持STM servlet使得allocate方法有点复杂。事实上,allocate方法有两部分,一部分迎合非-STM servlet和另一部分为STM servlets。第一部分有下面的代码框架。

if (!singleThreadModel) {
    // returns a non-STM servlet instance
}  

singleThreadModel是个布尔类型意味着StandardWrapper代表的servlet是一个STM servlet。singleThreadModel的初始化值是false,但是loadServlet方法在servlet加载时会测试servlet是否是STM servlet,并给设定值。loadServlet方法在"加载servlet"章节解释。

第二部分就是如果singleThreadModel为true就执行allocate方法。allocate方法将会分配一个STM servlet实例,只要该实例个数没有达到指定的最大个数。最大的STM 实例maxInstances默认是20个。StandardWrapper 提供了 nInstances 整型变量来定义当前 STM 实例的个数。

加载servlet

StandardWrapper类实现了Wrapper接口的load方法。load方法调用loadServlet方法,该方法加载servlet并调用init方法,传递一个javax.servlet.ServletConfig实例。下面是loaServlet的工作流程。

loadServlet首先检查当前StandardWrapper代表一个STM servlet。

加载servlet

StandardWrapper类实现了Wrapper接口的load方法。load方法调用loadServlet方法,该方法加载servlet并调用init方法,传递一个javax.servlet.ServletConfig实例。下面是loaServlet的工作流程。

loadServlet首先检查当前StandardWrapper代表一个STM servlet。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值