在 Servlet/JSP 中,如果想要進行會話管理,可以使用 HttpServletRequest
的 getSession()
方法取得 HttpSession
物件。例如:
HttpSession session = request.getSession();
getSession()
方法有兩個版本,另一個版本可以傳入布林值,預設是 true
,表示若尚未存在 HttpSession
實例時,直接建立一個新的物件。若傳入 false
,若尚未存在 HttpSession
實例,則直接傳回 null
。
HttpSession
上最常使用的方法大概就是 setAttribute()
與 getAttribute()
,從名稱上你應該可以猜到,這與 HttpServletRequest
(及 ServletContext
)的 setAttribute()
與 getAttribute()
類似,可以讓你在物件中設置及取得屬性。
如果你想要在瀏覽器與Web應用程式的會話期間,保留請求之間的相關訊息,則可以使用 HttpSession
的 setAttribute()
方法將相關訊息設置為屬性。在會話期間,就可以當作 Web 應用程式「記得」客戶端的資訊,如果想取出這些資訊,則透過 HttpSession
的 getAttribute()
就可以取出。
以下的範例是將〈隱藏欄位〉線上問卷,從隱藏欄位方式改用 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);
}
}
程式改寫時,分別利用 HttpSession
的 setAttribute()
來設置第一頁的問卷答案,以及 getAttribute()
來取得第一頁的問卷答案。從程式流程來看,不用考慮 HTTP 無狀態特性,而親自動手對瀏覽器發送隱藏欄位的 HTML。
預設在關閉瀏覽器前,所取得的 HttpSession
都是相同的實例。如果你想要在此次會話期間,直接讓目前的 HttpSession
失效,則可以執行 HttpSession
的 invalidate()
方法。一個使用的時機就是實作登出機制,如以下的範例所示範的,首先是登入的 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));
}
}
}
執行 HttpSession
的 invalidate()
之後,容器就會銷毀回收 HttpSession
物件,如果你再次透過 HttpServletRequest
的 getSession()
,取得的 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");
}
}
由於執行了 HttpSession
的 invalidate()
方法,當時的 HttpSession
失效,後續再取得新的 HttpSession
,當中當然不會有先前的 login
屬性,所以你再直接請求使用者頁面,就會因找不到 login
屬性,而被重新導向至登入表單。
注意,HttpSession
並非執行緒安全,所以必須注意屬性設定時共用存取的問題。最後,別忘了在這類使用者登入的資料傳送上,使用 HTTPS 加密連線。