天天看点

Tomcat的Session管理机制

http连接本身是无状态的,即前一次发起的连接跟后一次没有任何关系,是属于两次独立的连接请求,

但是互联网访问基本上都是需要有状态的,即服务器需要知道两次连接请求是不是同一个人访问的。

jsessionid是一个唯一标识号,用来标识服务器端的session,也用来标识客户端的cookie,客户端和服务器端通过这个jsessionid来一一对应。

客户端第一次请求到服务器连接,这个连接是没有附带任何东西的,没有cookie,没有jsessionid。

服务器端接收到请求后,会检查这次请求有没有传过来jsessionid或者cookie,如果没有jsessionid和cookie,则服务器端会创建一个session,

并生成一个与该session相关联的jsessionid返回给客户端,客户端会保存这个jsessionid,并生成一个与该jsessionid关联的cookie。

第二次请求的时候,会把该cookie(包含jsessionid)一起发送给服务器端,

这次服务器发现这个请求有了cookie,便从中取出jsessionid,然后根据这个jsessionid找到对应的session,这样便把http的无状态连接变成了有状态的连接。

但是有时候浏览器(即客户端)会禁用cookie,我们知道cookie是通过http的请求头部的一个cookie字段传过去的,

如果禁用,那么便得不到这个值,jsessionid便不能通过cookie传入服务器端。

这时可以通过url重写和隐藏表单,url重写就是把jsessionid附带在url后面传过去。隐藏表单是在表单提交的时候传入一个隐藏字段jsessionid。

session管理主要涉及到这几个方面:

创建session

注销session

持久化及启动加载session

tomcat通过每个context容器内的一个manager对象来管理session。

这个manager对象可以根据tomcat提供的接口或基类来自己定制,也可以使用tomcat的标准实现。

tomcat中的session管理主要在org.apache.catalina.session包中实现。

在具体说明session的创建过程之前,先看一下bs访问模型吧,这样理解直观一点。 

Tomcat的Session管理机制

browser发送http request;

tomcat内核http11processor会从http request中解析出“jsessionid”(具体的解析过程为先从request的url中解析,这是为了有的浏览器把cookie功能禁止后,将url重写考虑的,如果解析不出来,再从cookie中解析相应的jsessionid),解析完后封装成一个request对象(当然还有其他的http header);

servlet中获取session,其过程是根据刚才解析得到的jsessionid(如果有的话),从session池(session maps)中获取相应的session对象;这个地方有个逻辑,就是如果jsessionid为空的话(或者没有其对应的session对象,或者有session对象,但此对象已经过期超时),可以选择创建一个session,或者不创建;

如果创建新session,则将session放入session池中,同时将与其相对应的jsessionid写入cookie通过http response header的方式发送给browser,然后重复第一步。

以上是session的获取及创建过程。在servlet中获取session,通常是调用request的getsession方法。这个方法需要传入一个boolean参数,这个参数就是实现刚才说的,当jsessionid为空或从session池中获取不到相应的session对象时,选择创建一个新的session还是不创建。 

看一下核心代码逻辑;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

<code>protected</code> <code>session dogetsession(</code><code>boolean</code> <code>create) {</code>

<code>……</code>

<code>// 先获取所在context的manager对象</code>

<code>manager manager = </code><code>null</code><code>;</code>

<code>if</code> <code>(context != </code><code>null</code><code>)</code>

<code>manager = context.getmanager();</code>

<code>if</code> <code>(manager == </code><code>null</code><code>)</code>

<code>return</code> <code>(</code><code>null</code><code>); </code><code>// sessions are not supported</code>

<code>//这个requestedsessionid就是从http request中解析出来的</code>

<code>if</code> <code>(requestedsessionid != </code><code>null</code><code>) {</code>

<code>try</code> <code>{</code>

<code>//manager管理的session池中找相应的session对象</code>

<code>session = manager.findsession(requestedsessionid);</code>

<code>} </code><code>catch</code> <code>(ioexception e) {</code>

<code>session = </code><code>null</code><code>;</code>

<code>}</code>

<code>//判断session是否为空及是否过期超时</code>

<code>if</code> <code>((session != </code><code>null</code><code>) &amp;&amp; !session.isvalid())</code>

<code>if</code> <code>(session != </code><code>null</code><code>) {</code>

<code>//session对象有效,记录此次访问时间</code>

<code>session.access();</code>

<code>return</code> <code>(session);</code>

<code>// 如果参数是false,则不创建新session对象了,直接退出了</code>

<code>if</code> <code>(!create)</code>

<code>return</code> <code>(</code><code>null</code><code>);</code>

<code>if</code> <code>((context != </code><code>null</code><code>) &amp;&amp; (response != </code><code>null</code><code>) &amp;&amp;</code>

<code>context.getcookies() &amp;&amp;</code>

<code>response.getresponse().iscommitted()) {</code>

<code>throw</code> <code>new</code> <code>illegalstateexception</code>

<code>(sm.getstring(</code><code>"coyoterequest.sessioncreatecommitted"</code><code>));</code>

<code>// 开始创建新session对象</code>

<code>if</code> <code>(connector.getemptysessionpath()</code>

<code>&amp;&amp; isrequestedsessionidfromcookie()) {</code>

<code>session = manager.createsession(getrequestedsessionid());</code>

<code>} </code><code>else</code> <code>{</code>

<code>session = manager.createsession(</code><code>null</code><code>);</code>

<code>// 将新session的jsessionid写入cookie,传给browser</code>

<code>if</code> <code>((session != </code><code>null</code><code>) &amp;&amp; (getcontext() != </code><code>null</code><code>)</code>

<code>&amp;&amp; getcontext().getcookies()) {</code>

<code>cookie cookie = </code><code>new</code> <code>cookie(globals.session_cookie_name,</code>

<code>session.getidinternal());</code>

<code>configuresessioncookie(cookie);</code>

<code>response.addcookieinternal(cookie);</code>

<code>//记录session最新访问时间</code>

尽管不能贴出所有代码,但是上述的核心逻辑还是很清晰的。从中也可以看出,我们经常在servlet中这两种调用方式的不同;

<code>新创建session</code>

<code>request.getsession(); 或者request.getsession(</code><code>true</code><code>);</code>

<code>不创建session</code>

<code>request.getsession(</code><code>false</code><code>);</code>

接下来,看一下standardmanager的createsession方法,了解一下session的创建过程;

<code>public</code> <code>session createsession(string sessionid) {</code>

<code>是个session数量控制逻辑,超过上限则抛异常退出</code>

<code>if</code> <code>((maxactivesessions &gt;= </code><code>0</code><code>) &amp;&amp;</code>

<code>(sessions.size() &gt;= maxactivesessions)) {</code>

<code>rejectedsessions++;</code>

<code>(sm.getstring(</code><code>"standardmanager.createsession.ise"</code><code>));</code>

<code>return</code> <code>(</code><code>super</code><code>.createsession(sessionid));</code>

  

这个最大支持session数量maxactivesessions是可以配置的,先不管这个安全控制逻辑,看其主逻辑,即调用其基类的createsession方法;

<code>// 创建一个新的standardsession对象</code>

<code>session session = createemptysession();</code>

<code>// initialize the properties of the new session and return it</code>

<code>session.setnew(</code><code>true</code><code>);</code>

<code>session.setvalid(</code><code>true</code><code>);</code>

<code>session.setcreationtime(system.currenttimemillis());</code>

<code>session.setmaxinactiveinterval(</code><code>this</code><code>.maxinactiveinterval);</code>

<code>if</code> <code>(sessionid == </code><code>null</code><code>) {</code>

<code>//设置jsessionid</code>

<code>sessionid = generatesessionid();</code>

<code>session.setid(sessionid);</code>

<code>sessioncounter++;</code>

关键是jsessionid的产生过程,接着看generatesessionid方法;

<code>protected</code> <code>synchronized</code> <code>string generatesessionid() {</code>

<code>byte</code> <code>random[] = </code><code>new</code> <code>byte</code><code>[</code><code>16</code><code>];</code>

<code>string jvmroute = getjvmroute();</code>

<code>string result = </code><code>null</code><code>;</code>

<code>// render the result as a string of hexadecimal digits</code>

<code>stringbuffer buffer = </code><code>new</code> <code>stringbuffer();</code>

<code>do</code> <code>{</code>

<code>int</code> <code>resultlenbytes = </code><code>0</code><code>;</code>

<code>if</code> <code>(result != </code><code>null</code><code>) {</code>

<code>buffer = </code><code>new</code> <code>stringbuffer();</code>

<code>duplicates++;</code>

<code>while</code> <code>(resultlenbytes &lt; </code><code>this</code><code>.sessionidlength) {</code>

<code>getrandombytes(random);</code>

<code>random = getdigest().digest(random);</code>

<code>for</code> <code>(</code><code>int</code> <code>j = </code><code>0</code><code>;</code>

<code>j &lt; random.length &amp;&amp; resultlenbytes &lt; </code><code>this</code><code>.sessionidlength;</code>

<code>j++) {</code>

<code>byte</code> <code>b1 = (</code><code>byte</code><code>) ((random[j] &amp; </code><code>0xf0</code><code>) &gt;&gt; </code><code>4</code><code>);</code>

<code>byte</code> <code>b2 = (</code><code>byte</code><code>) (random[j] &amp; </code><code>0x0f</code><code>);</code>

<code>if</code> <code>(b1 &lt; </code><code>10</code><code>)</code>

<code>buffer.append((</code><code>char</code><code>) (</code><code>'0'</code> <code>+ b1));</code>

<code>else</code>

<code>buffer.append((</code><code>char</code><code>) (</code><code>'a'</code> <code>+ (b1 - </code><code>10</code><code>)));</code>

<code>if</code> <code>(b2 &lt; </code><code>10</code><code>)</code>

<code>buffer.append((</code><code>char</code><code>) (</code><code>'0'</code> <code>+ b2));</code>

<code>buffer.append((</code><code>char</code><code>) (</code><code>'a'</code> <code>+ (b2 - </code><code>10</code><code>)));</code>

<code>resultlenbytes++;</code>

<code>if</code> <code>(jvmroute != </code><code>null</code><code>) {</code>

<code>buffer.append(</code><code>'.'</code><code>).append(jvmroute);</code>

<code>result = buffer.tostring();</code>

<code>//注意这个do…while结构</code>

<code>} </code><code>while</code> <code>(sessions.containskey(result));</code>

<code>return</code> <code>(result);</code>

这里主要说明的不是生成jsessionid的算法了,而是这个do…while结构。把这个逻辑抽象出来,可以看出;

Tomcat的Session管理机制

如图所示,创建jsessionid的方式是由tomcat内置的加密算法算出一个随机的jsessionid,如果此jsessionid已经存在,则重新计算一个新的,直到确保现在计算的jsessionid唯一。

好了,至此一个session就这么创建了,像上面所说的,返回时是将jsessionid以http response的header:“set-cookie”发给客户端。

主动注销

session创建完之后,不会一直存在,或是主动注销,或是超时清除。即是出于安全考虑也是为了节省内存空间等。

例如,常见场景:用户登出系统时,会主动触发注销操作。 

主动注销时,是调用标准的servlet接口: 

<code>session.invalidate();</code>

看一下tomcat提供的标准session实现(standardsession)

<code>public</code> <code>void</code> <code>invalidate() {</code>

<code>if</code> <code>(!isvalidinternal())</code>

<code>(sm.getstring(</code><code>"standardsession.invalidate.ise"</code><code>));</code>

<code>// 明显的注销方法</code>

<code>expire();</code>

expire方法的逻辑稍后再说,先看看超时注销,因为它们调用的是同一个expire方法。

超时注销

tomcat定义了一个最大空闲超时时间,也就是说当session没有被操作超过这个最大空闲时间时间时,再次操作这个session,这个session就会触发expire。 

这个方法封装在standardsession中的isvalid()方法内,这个方法在获取这个request请求对应的session对象时调用,可以参看上面说的创建session环节。

也就是说,获取session的逻辑是,先从manager控制的session池中获取对应jsessionid的session对象,如果获取到,就再判断是否超时,如果超时,就expire这个session了。 

<code>public</code> <code>boolean</code> <code>isvalid() {</code>

<code>//这就是判断距离上次访问是否超时的过程</code>

<code>if</code> <code>(maxinactiveinterval &gt;= </code><code>0</code><code>) {</code>

<code>long</code> <code>timenow = system.currenttimemillis();</code>

<code>int</code> <code>timeidle = (</code><code>int</code><code>) ((timenow - thisaccessedtime) / 1000l);</code>

<code>if</code> <code>(timeidle &gt;= maxinactiveinterval) {</code>

<code>expire(</code><code>true</code><code>);</code>

<code>return</code> <code>(</code><code>this</code><code>.isvalid);</code>

expire方法 

是时候来看看expire方法了。

<code>public</code> <code>void</code> <code>expire(</code><code>boolean</code> <code>notify) {</code>

<code>synchronized</code> <code>(</code><code>this</code><code>) {</code>

<code>......</code>

<code>//设立标志位</code>

<code>setvalid(</code><code>false</code><code>);</code>

<code>//计算一些统计值,例如此manager下所有session平均存活时间等</code>

<code>int</code> <code>timealive = (</code><code>int</code><code>) ((timenow - creationtime)/</code><code>1000</code><code>);</code>

<code>synchronized</code> <code>(manager) {</code>

<code>if</code> <code>(timealive &gt; manager.getsessionmaxalivetime()) {</code>

<code>manager.setsessionmaxalivetime(timealive);</code>

<code>int</code> <code>numexpired = manager.getexpiredsessions();</code>

<code>numexpired++;</code>

<code>manager.setexpiredsessions(numexpired);</code>

<code>int</code> <code>average = manager.getsessionaveragealivetime();</code>

<code>average = ((average * (numexpired-</code><code>1</code><code>)) + timealive)/numexpired;</code>

<code>manager.setsessionaveragealivetime(average);</code>

<code>// 将此session从manager对象的session池中删除</code>

<code>manager.remove(</code><code>this</code><code>);</code>

不需要解释,已经很清晰了。

这个超时时间是可以配置的,缺省在tomcat的全局web.xml下配置,也可在各个app下的web.xml自行定义;

<code>&lt;</code><code>session-config</code><code>&gt;</code>

<code>&lt;</code><code>session-timeout</code><code>&gt;30&lt;/</code><code>session-timeout</code><code>&gt;</code>

<code>&lt;/</code><code>session-config</code><code>&gt; </code>

单位是分钟。

这个功能主要是,当tomcat执行安全退出时(通过执行shutdown脚本),会将session持久化到本地文件,通常在tomcat的部署目录下有个session.ser文件。

Tomcat的Session管理机制

当启动tomcat时,会从这个文件读入session,并添加到manager的session池中去。

这样,当tomcat正常重启时, session没有丢失,对于用户而言,体会不到重启,不影响用户体验。

看一下概念图吧,觉得不是重要实现逻辑,代码就不说了。

Tomcat的Session管理机制

由此可以看出,session的管理是容器层做的事情,应用层一般不会参与session的管理,也就是说,如果在应用层获取到相应的session,已经是由tomcat提供的,

因此如果过多的依赖session机制来进行一些操作,例如访问控制,安全登录等就不是十分的安全,因为如果有人能得到正在使用的jsessionid,则就可以侵入系统。

继续阅读