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通路模型吧,這樣了解直覺一點。

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>) && !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>) && (response != </code><code>null</code><code>) &&</code>
<code>context.getcookies() &&</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>&& 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>) && (getcontext() != </code><code>null</code><code>)</code>
<code>&& 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 >= </code><code>0</code><code>) &&</code>
<code>(sessions.size() >= 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 < </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 < random.length && resultlenbytes < </code><code>this</code><code>.sessionidlength;</code>
<code>j++) {</code>
<code>byte</code> <code>b1 = (</code><code>byte</code><code>) ((random[j] & </code><code>0xf0</code><code>) >> </code><code>4</code><code>);</code>
<code>byte</code> <code>b2 = (</code><code>byte</code><code>) (random[j] & </code><code>0x0f</code><code>);</code>
<code>if</code> <code>(b1 < </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 < </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結構。把這個邏輯抽象出來,可以看出;
如圖所示,建立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 >= </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 >= 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 > 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><</code><code>session-config</code><code>></code>
<code><</code><code>session-timeout</code><code>>30</</code><code>session-timeout</code><code>></code>
<code></</code><code>session-config</code><code>> </code>
機關是分鐘。
這個功能主要是,當tomcat執行安全退出時(通過執行shutdown腳本),會将session持久化到本地檔案,通常在tomcat的部署目錄下有個session.ser檔案。
當啟動tomcat時,會從這個檔案讀入session,并添加到manager的session池中去。
這樣,當tomcat正常重新開機時, session沒有丢失,對于使用者而言,體會不到重新開機,不影響使用者體驗。
看一下概念圖吧,覺得不是重要實作邏輯,代碼就不說了。
由此可以看出,session的管理是容器層做的事情,應用層一般不會參與session的管理,也就是說,如果在應用層擷取到相應的session,已經是由tomcat提供的,
是以如果過多的依賴session機制來進行一些操作,例如通路控制,安全登入等就不是十分的安全,因為如果有人能得到正在使用的jsessionid,則就可以侵入系統。