跳到主要內容

JavaEE Session、Cookie處理

last update:2013/6

一般的網頁連線過程裡,server端與client端的互動就像丟球遊戲一般
由client端丟球(連線)到server端,server端再根據過來的資訊把球丟給(回傳資訊)client端
而每一次的連線行為都是獨立的
server端專注接球並把球傳回去,並不會注意是誰把球丟過來

如果過程中有需求要持續辨識使用者的身分,則會透過Cookie、Session兩種機制來辨識
Cookie是將資訊記錄在client端,並連線時將訊息一併傳遞給server端
用比喻來形容就是在球上面簽名,server端接到球後就會知道是誰把球丟過來
Session則是將資訊存放在server端的記憶體中,並依此辨識
比喻是接球的server端用大腦記著每一個丟球給他的人

當然以上比喻對於細節也許不是那麼符合
而當系統對連線的辨識需求較多,或是即時性的要求較高(例如:twitter、線上聊天等系統)
使用Cookie、Session機制就不是那麼有效率了
包含HTML5也提出WebSocket機制來處理這類需求,而過去也有幾種標準作法
介紹可以參考JosephJ大的文章 - Browser 與 Server 持續同步的作法介紹

JavaEE的Cookie原始設計是用來協助session,它的形式是 Key / Value 配對
Server要求瀏覽器在Client端建立Cookie,並在需要的時候從Client端取得
Java的Cookie預設的存活時間僅限定在瀏覽器開啟時,一旦關閉瀏覽器就隨之消滅
如果想讓它有長時間的效用,可以手動設定其存活
範例如下:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                       throws ServletException, IOException {
        response.setContentType("text/html");
        String name = request.getParameter("username");

        Cookie cookie = new Cookie("username", name);
        cookie.setMaxAge(30 * 60); //set Life of Cookie to 30 minutes
        response.addCookie(cookie);
    }

瀏覽器協助建立Cookie時,會依照網站別建立不同的儲存資料夾
所以不用擔心不同網站間有相同Key值的Cookie的會相互影響
特別注意一點是每一個連到Server的 request 都會夾帶相同domain下的Cookie
所以當Cookie越來越多時,也會使 request 變的龐大,並影響網站的使用頻寬
如果單純只是在 Client-Side 執行的資料存取,可以使用 HTML5 的 localStorage、sessionStorage
這樣可以減少 request 的大小,提升網站讀取速度及降低頻寬使用量
如果是前後端共同處理則依然要用 Cookie,這與Java EE 在Client-Side的存取權限有關

取得Cookie時,也如同建立時一般,作用頁面僅能取得相同網域下的Cookie
要在其中取得欲查詢的 Key / Value 配對,必須透過迴圈比對getCookies()取得的Cookie陣列
要讓Cookie立即失效, 只要將存活時間設定成 0 即可
存活時間設定成負值則是讓Cookie在瀏覽器關閉後失效
將過程封裝成函式如下:
     public boolean isCookieSet(HttpServletRequest request) {
         Cookie[] cookies = request.getCookies();

         if (cookies != null) {
             for (int i = 0; i < cookies.length; i++) {
                 Cookie cookie = cookies[i];
                 String name = cookie.getName();
                 String value = cookie.getValue();
             }
         }....
     }

這樣就能在JSP頁面中用 <%=xxx.isCookieSet(request) %> 的形式使用它
xxx 指的是在這個頁面中使用的 bean 名稱,request 是JSP的隱含物件,後續會再提到相關使用

通常建立的Cookie不會限定讀取它的對象
Servlet3.0 Cookie新增了setHttpOnly()
這個方法會在設定指定的Cookie時,在request的 header上加上HttpOnly屬性
設定此屬性的Cookie在瀏覽器支援此標準時,將不會開放Cookie給JavaScript讀取
可以用isHttpOnly()方法檢測Cookie是否有設定HttpOnly屬性

Cookie因為放置在 Client偳,所以也有被修改的可能
基於安全性考量,重要操作的身分辨識不宜使用Cookie機制,例如銀行的線上系統就不該倚賴Cookie
Session的機制因為資料儲存在Server的記憶體中,所以安全性會好一些
Session的原理是給予每個訪問一個隨機產生的session ID,並藉由它記錄資訊
當然若依此架構進行,由同一台電腦的相同瀏覽器發出的重複訪問理應也會各自被給予session ID
要解決這個問題,必須想出一個方法讓每個訪問能夠和Server上的session ID產生關聯
答案就是前面提到的Cookie的原始設計目的,預設使用Cookie儲存session ID

正常情況下,也就是 Client 端沒關閉Cookie的功能時
Client 端會建立一個叫做 jsessionid 的Cookie來儲存container給予的session ID
Server 端會根據這個Cookie裡的資料來對應產生的session
撰寫的程式不應該自行請求 jsessionid 參數( getParameter("jsessionid") )
也不應該在 header 裡增加 jsessionid 屬性,以免Cookie被覆寫
應該由container代為處理的行為千萬不要自己修改
程式碼範例:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
       
        HttpSession session = request.getSession();
       
        if(session.isNew()){
            out.println("A new session");
        }else{
            out.println("Welcome back ! ");
        }
    } 

request.getSession()這行程式碼會回傳session
尚未有session的情況下,則會自動產生session ID並以它建立session後回傳
getSession() 裡接受boolean型別參數,能做出篩選已存在session的效果
getSession(true) 跟 getSession()作用相同
getSession(false) 表示希望得到已經存在的session,找不到時會回傳null值
session.isNew() 則如同它字面上的意思,若Client端還沒使用過這個session時會回傳true

如果Client端關閉Cookie的使用,會無法加入session
session.isNew()會一直回傳 true,也不會有警告訊息提醒無法將session關聯到 Client 端
解決Cookie設定關閉的方法是在URL上面附加辨識身分的資訊來協助對應session
範例程式碼:
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        //to get session if Cookie function is open
        HttpSession session = request.getSession();
       
        out.println("<html><body>");
        out.println("<a href=\"" + response.encodeURL("destination")
                    + "\">click me</a>");
        out.println("</body></html>");
    }

encodeURL()會在無法取得帶有sessionID的cookie時,自動產生帶有session ID的URL rewrite

URL的 rewrite 只有在container嘗試取得Cookie失敗,及有呼叫response物件的 encodeURL() 後才發生
container讀到 getSession() 後,會同時使用Cookie和附加資訊在URL上
接著由Client端傳來下一個請求時,getSession()會讓 container 從請求中讀取session ID
若可以藉由Cookie取得,container會忽略 encodeURL() 的呼叫,也省下重寫URL花費的成本

需要使用重導( Redirect )時的處理方法也很類似
以response.encodeRedirectURL("destination")來重寫URL
URL重寫的處理方式由container的廠商提供,一般開發者不用注意其如何被實作
另外,URL rewrite 不會作用在純HTML的靜態頁面

session的使用需求除了儲存身份資料供辨識外
也常像 request、response物件一樣使用getAttribute()、setAttribute()、removeAttribute()設定屬性
藉此也能做到傳遞該使用者的特定資料的效果

session物件儲存在Server的記憶體裡
閒置的session會浪費系統資源,所以也有一些和其存活時間相關的方法可以使用
1.getCreationTime() 傳回session第一次被建立的時間
   計算session到目前為止的存活時間,可以做到限制session的存活時間的效果

2.getLastAccessedTime() 回傳container最後一次收到與此session ID相關的請求的時間
   可以判斷客戶是否離開很長的時間,並提醒客戶或是直接用 invalidate() 砍掉session
 
3.setMaxInactiveInterval() 指定針對此session的不同請求的最長時間間隔
   客戶若沒有在指定時間內做出請求,會讓此session失效,時間單位是秒

4.getMaxInactiveInterval() 取得針對此session的不同請求的最長時間間隔
   可以用來判斷一個不活動的客戶端的session還有多少的存活時間
 
5.invalidate() 立刻結束此session並釋放資源
   讓特定session立即失效並清除其所擁有的屬性。當session已經被清除時會引發例外

除了使用上述方法主動砍掉session外,container也提供了清除session的設定機制
上面提到的 setMaxInactiveInterval() ,會讓沒有在指定的秒數內作出回應的session被清除
另外也可以在web.xml裡組態設定,讓container主動回收沒有在指定時間內做出回應的session
範例:
web.xml================

............
  </servlet>

  <session-config>
      <session-timeout>20</session-timeout>
  </session-config>
............

不過需要注意的是,setMaxInactiveInterval()與DD兩方法的輸入參數的時間單位不同
setMaxInactiveInterval()裡需填入存活秒數,組態所設定的是存活的分鐘數


Servlet3.0新增SessionCookieConfig介面
可以透過ServletContext的getSessionCookieConfig()取得實作該介面的物件
ServletContext會在後續提到,可以到時候再回過頭來看這段
透過這個實作物件可以設定儲存session ID的Cookie的相關資訊
例如setName()可以修改預設的session ID名稱
setAge()可以修改此類Cookie的存活秒數
注意SessionCookieConfig的設定必須在ServletContext初始化前完成
所以上述方法的適用時機是ServletContextListener的ContextInitialized()
在其中取得Servletcontext後進行設定

另一種做法是在web.xml中設定

............
  </servlet>

  <session-config>
      <session-timeout>20</session-timeout>
      <cookie-config>
           <name>xxx</name>
           <http-only>true</http-only>
      </cookie-config>
  </session-config>
............


session搬遷則是HttpSession物件裡重要的議題之一,在大型專案較有機會處理到
大型專案的分散式結構會為了達到平衡負載的目的,而可能將請求分別送往不同的JVM中
因此 Client 端對同一個Servlet所提出的複數個請求很有可能會在不同的JVM上
如何處理session也會是一個課題

HttpSession物件的處理要遵守其原則
應用程式裡的每個session ID都只會對應到一個HttpSession物件
在同個瀏覽器裡的請求一定會有發生的先後順序
所以HttpSession物件會依照請求順序搬移到該請求所在的JVM上
之後會在Listener部分筆記提到在Java EE中的實作機制

實務運用上,只要屬性是可序列化(Serializable)的,它也可以跟著session一起被搬遷
實作Serializable介面的物件可以選擇實作 writeObject() 和 readObject() 兩個method
writeObject()可以在序列化期間將不可序列化的欄位變成 null,並在 readObject()時將之恢復
然而,這些方法不一定會在session搬遷期間被呼叫,實作依自身需求及時機決定

留言