使用 HttpSession


在 Servlet/JSP 中,如果想要進行會話管理,可以使用 HttpServletRequestgetSession() 方法取得 HttpSession 物件。例如:

HttpSession session = request.getSession();

getSession() 方法有兩個版本,另一個版本可以傳入布林值,預設是 true,表示若尚未存在 HttpSession 實例時,直接建立一個新的物件。若傳入 false,若尚未存在 HttpSession 實例,則直接傳回 null

HttpSession 上最常使用的方法大概就是 setAttribute()getAttribute(),從名稱上你應該可以猜到,這與 HttpServletRequest(及 ServletContext)的 setAttribute()getAttribute() 類似,可以讓你在物件中設置及取得屬性。

如果你想要在瀏覽器與Web應用程式的會話期間,保留請求之間的相關訊息,則可以使用 HttpSessionsetAttribute() 方法將相關訊息設置為屬性。在會話期間,就可以當作 Web 應用程式「記得」客戶端的資訊,如果想取出這些資訊,則透過 HttpSessiongetAttribute() 就可以取出。

以下的範例是將〈隱藏欄位〉線上問卷,從隱藏欄位方式改用 HttpSession 方式來實作會話管理:

package cc.openhome;

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/questionnaire")
public class Questionnaire extends HttpServlet {
    protected void processRequest(HttpServletRequest request, HttpServletResponse response) 
                      throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head>");
        out.println("<meta charset='UTF-8'>");
        out.println("</head>");
        out.println("<body>");

        String page = request.getParameter("page");
        out.println("<form action='questionnaire' method='post'>");

        if("page1".equals(page)) {          // 第一頁問卷
            out.println("問題一:<input type='text' name='p1q1'><br>");
            out.println("問題二:<input type='text' name='p1q2'><br>");
            out.println("<input type='submit' name='page' value='page2'>");
        }
        else if("page2".equals(page)) {    // 第二頁問卷
            String p1q1 = request.getParameter("p1q1");
            String p1q2 = request.getParameter("p1q2");
            request.getSession().setAttribute("p1q1", p1q1);
            request.getSession().setAttribute("p1q2", p1q2);
            out.println("問題三:<input type='text' name='p2q1'><br>");
            out.println("<input type='submit' name='page' value='finish'>");
        }
        else if("finish".equals(page)) {    // 最後答案收集
            out.println(request.getSession().getAttribute("p1q1") + "<br>");
            out.println(request.getSession().getAttribute("p1q2") + "<br>");
            out.println(request.getParameter("p2q1") + "<br>");
        }
        out.println("</form>");
        out.println("</body>");
        out.println("</html>");
    } 

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        processRequest(request, response);
    } 

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        processRequest(request, response);
    }
}

程式改寫時,分別利用 HttpSessionsetAttribute() 來設置第一頁的問卷答案,以及 getAttribute() 來取得第一頁的問卷答案。從程式流程來看,不用考慮 HTTP 無狀態特性,而親自動手對瀏覽器發送隱藏欄位的 HTML。

預設在關閉瀏覽器前,所取得的 HttpSession 都是相同的實例。如果你想要在此次會話期間,直接讓目前的 HttpSession 失效,則可以執行 HttpSessioninvalidate() 方法。一個使用的時機就是實作登出機制,如以下的範例所示範的,首先是登入的 Servlet 實作:

package cc.openhome;

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/login")
public class Login extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
        String name = request.getParameter("name");
        String passwd = request.getParameter("passwd");
        if("caterpillar".equals(name) && "123456".equals(passwd)) {
            if(request.getSession(false) != null) {
                request.changeSessionId();
            }
            request.getSession().setAttribute("login", name);
            response.sendRedirect("user");
        }
        else {
            response.sendRedirect("login.html");
        }
    }
} 

在登入時,如果名稱與密碼正確,就取得 HttpSession,基於 Web 安全考量,建議在登入成功後改變 Session ID,原理在之後的文件中會說明,想改變 Session ID,可以透過 Servlet 3.1 於 HttpServletRequest 上新增的 changeSessionId() 來達到。

至於 Servlet 3.0 之前的版本,必須自行取出 HttpSession 中的屬性,令目前的 HttpSession 失效,然後取得 HttpSession 並設定屬性,例如:

package cc.openhome;

import java.io.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/login")
public class Login extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
        String name = request.getParameter("name");
        String passwd = request.getParameter("passwd");
        if("caterpillar".equals(name) && "123456".equals(passwd)) {
            if(request.getSession(false) != null) {
                changeSessionId(request);
            }               
            request.getSession().setAttribute("login", name);
            response.sendRedirect("user");
        }
        else {
            response.sendRedirect("login.html");
        }
    }

    private void changeSessionId(HttpServletRequest request) {
        HttpSession oldSession = request.getSession();

        Map<String, Object> attrs = new HashMap<>();
        for(String name : Collections.list(oldSession.getAttributeNames())) {
            attrs.put(name, oldSession.getAttribute(name));
            oldSession.removeAttribute(name);
        }
        oldSession.invalidate(); // 令目前 Session 失效

        HttpSession newSession = request.getSession(); 
        for(String name : attrs.keySet()) {
            newSession.setAttribute(name, attrs.get(name));
        }
    }
} 

執行 HttpSessioninvalidate() 之後,容器就會銷毀回收 HttpSession 物件,如果你再次透過 HttpServletRequestgetSession(),取得的 HttpSession 就是另一個新的物件了。

登入成功之後,為了之後免於重複驗證使用者是否登入,可以設定一個 login 屬性,用以代表使用者作完成登入的動作,其他的 Servlet/JSP 如果可以從 HttpSession 取得 login 屬性,基本上就可以確定是個已登入的使用者,這類用來辨識使用者是否登入的屬性,通常稱之為登入字符(Login Token)。上面這個範例在登入成功之後,會轉發至使用者頁面:

package cc.openhome;

import java.io.*;
import java.util.Optional;
import java.util.stream.Stream;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/user")
public class User extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                       throws ServletException, IOException {

        HttpSession session = request.getSession();
        Optional<Object> token = Optional.ofNullable(session.getAttribute("login"));

        if(token.isPresent()) {
            userHtml(request, response);
        } else {
            response.sendRedirect("login.html");
        }
    }

    private void userHtml(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html>");
        out.println("<head>");
        out.println("<meta charset='UTF-8'>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1>" + request.getSession().getAttribute("login") + "已登入</h1><br>");
        out.println("<a href='logout'>登出</a>");
        out.println("</body>");
        out.println("</html>");
    }
} 

如果有瀏覽器請求使用者頁面,程式會先嘗試取得 HttpSession 中的 login 屬性,如果無法取得,表示使用者尚未登入,則要求瀏覽器重新導向至登入表單,使用 Token 的方式來確認使用者是否登入,只是免於處處要求使用者進行驗證的困擾,然而,重要或敏感性的操作之前,最好再次進行身份確認,像是要求另一組密碼之類的。

如果可以取得 login 屬性,則顯示使用者頁面,頁面中有一個可以執行登出的 URL 超鏈結,按下後會執行以下的程式。

package cc.openhome;

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/logout")
public class Logout extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
         request.getSession().invalidate();
         response.sendRedirect("login.html");
    } 
}

由於執行了 HttpSessioninvalidate() 方法,當時的 HttpSession 失效,後續再取得新的 HttpSession,當中當然不會有先前的 login 屬性,所以你再直接請求使用者頁面,就會因找不到 login 屬性,而被重新導向至登入表單。

注意,HttpSession 並非執行緒安全,所以必須注意屬性設定時共用存取的問題。最後,別忘了在這類使用者登入的資料傳送上,使用 HTTPS 加密連線。