天天看點

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,則就可以侵入系統。

繼續閱讀