JavaWeb高階程式設計(五)—— 使用會話來維持HTTP狀態
一、需要會話的原因
從伺服器的角度來說,當請求結束時,客戶端與伺服器之間就再有任何聯絡,如果有下一個請求開始時,就無法將新的請求與之前的請求關聯起來。這是因為 HTTP請求自身是完全無狀態的,會話就是用來維持請求和請求之間的狀態的。
拿生活場景舉例:你進入最喜歡的超市購物,找到一個購物車(從伺服器獲得會話),一邊逛一邊挑選喜愛的商品並將它們新增到購物車中(將商品新增到會話中),購物結束後將購物車中的商品取出並遞給收銀員,收銀員掃描你購買的商品並接收你的付款(通過會話付款),付款結束後出門並還回購物車(關閉瀏覽器或登出,結束會話)。
二、使用會話cookie和URL重寫
會話是由伺服器或Web應用程式管理的某些檔案、記憶體片段、物件或者容器,它包含了分配給它的各種不同的資料。這些資料元素可以是使用者名稱、購物車、工作流細節等,使用者瀏覽器中不用保持或維持任何此類資料,它們只由伺服器或Web應用程式程式碼管理。容器和使用者瀏覽器之間將通過某種方式連線起來,因此通常會話會被賦予一個隨機生成的字串,稱為會話ID。
第一次建立會話時,伺服器建立的會話ID將會作為響應的一部分返回到使用者瀏覽器中,接下來從使用者瀏覽器中發出的請求都將通過某種方式包含該會話ID,當應用程式收到含有會話ID的請求時,它可以通過該ID將會話與當前請求關聯起來。剩下需要解決的問題就是如何將會話ID從伺服器返回到瀏覽器中,並在之後的請求中包含該ID,目前有兩種可用的技術:會話cookie和URL重寫。
1、瞭解會話cookie
cookie是一種必要的通訊機制,可以通過Set-Cookie響應頭在伺服器和瀏覽器之間傳遞任意的資料,並存儲在使用者計算機中,然後再通過請求頭Cookie從瀏覽器返回到伺服器中。cookie可以有各種不同的特性,如下:
① Domain:告訴瀏覽器應該將cookie傳送到哪個域名中;
② Path:進一步將cookie限制在相對於域的某個特定URL中,每次瀏覽器發出請求時,它都將找到匹配該域和路徑的所有cookie;
③ Expires:與Max-age,HTTP/1.0會優先處理該指令,它定義了cookie的絕對過期日期,如果cookie已經過期,瀏覽器將會立即刪除它;
④ Max-age:與Expires互斥,HTTP/1.1會優先處理該指令,它定義了cookie將在多少秒後過期,如果cookie中不含有Expires和Max-age,cookie將會瀏覽器關閉時被刪除;
⑤ Secure:(不需要有值),瀏覽器將只會通過HTTP傳送cookie,這將保護cookie,避免以未加密的方式進行傳輸;
⑥ HttpOnly:它把cookie限制在直接的瀏覽器請求中,使得JavaScript、Flash以及其他外掛無法訪問cookie。
注意:儘管HttpOnly將阻止JavaScript使用doucment.cookie屬性來訪問cookie,但由JavaScript建立的AJAX請求仍然會包含會話ID cookie,因為是瀏覽器負責AJAX請求頭的生成而不是JavaScript,這意味著伺服器仍然能夠將AJAX請求關聯到使用者的會話。
當Web伺服器和應用伺服器使用cookie在客戶端儲存會話ID時,這些ID將隨著每次的請求被髮送到伺服器端,在JAVAEE應用伺服器中,會話cookie的名字預設為JSESSIONID
2、URL中的會話ID
JavaEE伺服器將會話ID新增到URL的最後一個路徑段的矩陣引數中,通過這種方式分離開會話ID與查詢字串的引數,使它們不會互相沖突。
請求URL只會在將會話ID從瀏覽器傳送到伺服器時有效,那麼第一次如何將請求URL中的會話ID從伺服器傳送到瀏覽器呢?答案是必須將會話ID內嵌在應用程式返回的所有URL中,包括頁面的連結、表單操作以及302重定向。
HttpServletResponse介面定義了兩個可以重寫URL的方法:encodeURL和encodeRedirectURL,它們將在必要的時候將會話ID內嵌在URL中,任何在連結、表單操作或其他標籤中的URL都將被傳入到encodeURL方法中,然後該方法將會返回一個正確的、經過編碼處理的URL(任何傳入sendRedirect響應方法中的URL可以傳入encodeRedirectURL方法中)。將JSESSIONID矩陣引數內嵌在URL的最後一個路徑段中需要滿足下面4個條件:
① 會話對於當前請求是活躍的(要麼它通過傳入會話ID的方式請求會話,要麼應用程式建立了一個新的會話);
② JSEESSIONID cookie在請求中不存在;
③ URL不是絕對的URL,並且是同一Web應用程式中的URL;
④ 在部署描述符中已經啟用了對會話URL重寫的支援。
3、會話的漏洞
⑴ 複製並貼上錯誤
不知情的使用者決定要跟朋友分享應用程式中的某個頁面,並將位址列中的URL複製粘貼出來,那麼他的朋友將看到URL中包含的會話ID,如果他們在該會話終結之前訪問該URL,那麼他們也會被伺服器當成之前分享URL的使用者,這明顯會引起問題。
解決此問題的方法是:完全禁止在URL中內嵌會話ID。
⑵ 會話固定
攻擊者會首先找到允許在URL中內嵌會話ID的網站,然後獲取一個會話ID,並將含有會話ID的URL傳送給目標使用者。此時,當用戶點選連結進入網站時,他的會話ID就變成了URL中已經含有的會話ID(攻擊者已經持有的會話ID),如果使用者接著在該會話期間登入網站,那麼攻擊者也可以登入成功。
解決這個問題的兩種方法:一是同複製貼上一樣禁止在URL中內嵌會話ID,二是在登入後採用會話遷移,即當用戶登入後,修改會話ID或者將之前的會話資訊複製到一個新的會話中,並使之前的會話無效。
⑶ 跨站指令碼和會話劫持
攻擊者將利用網站的漏洞實行跨站指令碼攻擊,將JavaScript注入到某個頁面,通過document.cookie讀取會話ID cookie中的內容,當攻擊者從使用者處獲得會話ID後,他可以通過在自己的計算機中建立cookie來模擬該會話。
解決這個問題的方法是:不要在網站中使用跨站指令碼,並在所有的cookie中使用HttpOnly特性。
⑷ 不安全的cookie
最後一個需要考慮的是中間人攻擊(MitM攻擊),這是典型的資料截獲攻擊,攻擊者通過觀察客戶端和服務端的互動或響應,從中獲取資訊。
解決這個問題的方法是:使用HTTPS來保護網路通訊,cookie的secure標誌將告訴瀏覽器只應該通過HTTPS傳輸cookie。
三、在會話中儲存資料
1、在部署描述符中配置會話
下面是一份詳細的關於在部署描述符中配置會話的說明:
<!--下面標籤中的標籤選項都是可選的,如果選擇了要按照一定的順序新增-->
<session-config>
<!--會話的超時時間(單位:分鐘),Tomcat預設30分鐘,如果此值小於0將永遠不會過期-->
<session-timeout>30</session-timeout>
<!--只有在追蹤模式中使用了cookie時,才可以使用此標籤-->
<cookie-config>
<!--預設值,不需要修改-->
<name>JSESSIONID</name>
<!--domain標籤和path標籤對應著cookie的Domain和Path特性,Web容器已經設定了正確的預設值,通常不需要修改它們-->
<domain>example.org</domain>
<path>/shop</path>
<!--此標籤內可以新增任意文字-->
<comment>other info</comment>
<!--此標籤對應cookie的HttpOnly特性,預設為false,為了提高安全性,應將其設為true-->
<http-only>true</http-only>
<!--此標籤對應cookie的Secure特性,預設為false,如果使用了HTTPS,應將其設為true-->
<secure>false</secure>
<!--此標籤對應cookie的Max-Age特性,用於控制cookie的過期時間(單位:秒),預設沒有過期時間(相當於設定為-1),
即瀏覽器關閉就過期。最好保持預設值不變,正常情況下也不要去使用這個標籤-->
<max-age>1800</max-age>
</cookie-config>
<!--表示容器的追蹤策略,可以設定一個或多個策略,從上到下安全等級遞增,當使用了更高安全等級的策略就不可再使用低等級的策略-->
<tracking-mode>URL</tracking-mode>
<tracking-mode>COOKIE</tracking-mode>
<tracking-mode>SSL</tracking-mode>
</session-config>
下面是一份常用的關於在部署描述 符中配置會話的簡單配置:
<!--該配置設定會話過期時間為30分鐘,追蹤策略為COOKIE,使用HttpOnly特性來解決安全問題,其他的將接受預設值-->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
2、儲存和獲取資料
⑴ 在servlet中使用會話
先建立一個StoreServlet繼承HttpServlet,然後新增三個方法,示例如下:
/**
* author Alex
* date 2018/10/27
* description 在servlet中建立一個簡單的map,用於表示產品資料庫
*/
@WebServlet(
name = "storeServlet",
urlPatterns = "/shop"
)
public class StoreServlet extends HttpServlet {
private final Map<Integer,String> products = new HashMap<>();
public StoreServlet(){
products.put(1,"桌子");
products.put(2,"椅子");
products.put(3,"床");
products.put(4,"電視");
products.put(5,"洗衣機");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getParameter("action");
if(null == action){
action = "browse";
}
switch (action){
case "addToCart":
addToCart(request,response);
break;
case "showCart":
showCart(request,response);
break;
case "browse":
default:
browse(request,response);
break;
}
}
private void showCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("products",products);
request.getRequestDispatcher("/WEB-INF/jsp/showCart.jsp").forward(request,response);
}
private void browse(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("products",products);
request.getRequestDispatcher("/WEB-INF/jsp/browse.jsp").forward(request,response);
}
private void addToCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int productId;
try {
productId = Integer.parseInt(request.getParameter("productId"));
}catch (Exception e){
e.printStackTrace();
response.sendRedirect("shop");
return;
}
HttpSession session = request.getSession();
if(null == session.getAttribute("cart")){
session.setAttribute("cart",new Hashtable<Integer,Integer>());
}
Map<Integer,Integer> cart = (Map<Integer,Integer>)session.getAttribute("cart");
if(cart.containsKey(productId)){
cart.put(productId,cart.get(productId)+1);
}else {
cart.put(productId,0);
}
response.sendRedirect("shop?action=showCart");
}
}
⑵ 在JSP中使用會話
首先,建立WEB-INF/jsp/browse.jsp檔案,示例如下:
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>商品列表</title>
</head>
<body>
<h2>商品展示列表</h2>
<%
Map<Integer,String> products = (Map<Integer,String>) request.getAttribute("products");
for(Integer id:products.keySet()){
%><a href="<c:url value="/shop">
<c:param name="action" value="addToCart"/>
<c:param name="productId" value="<%=id.toString()%>"/>
</c:url>"><%=products.get(id)%><br></a><%
}
%>
<br>
<h2>
<a href="<c:url value="/shop?action=showCart"/>">購物車</a>
</h2>
</body>
</html>
然後,建立WEB-INF/jsp/showCart.jsp檔案,示例如下:
<%@ page import="java.util.Map" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<html>
<head>
<title>購物車</title>
</head>
<body>
<h2>購物車展示列表</h2>
<%
Map<Integer,String> products = (Map<Integer,String>)request.getAttribute("products");
Map<Integer,Integer> cart = (Map<Integer,Integer>)session.getAttribute("cart");
if(null == cart || cart.size() ==0){
%>購物車為空<%
}else {
for(Integer id:cart.keySet()){
%>商品名:<%=products.get(id)%> 數量:<%=cart.get(id)%><br><%
}
}
%>
<br>
<h2>
<a href="<c:url value="/shop"/>">商品展示列表</a>
</h2>
</body>
</html>
3、刪除資料
首先,在StoreServlet中再新增一個清空購物車的方法實現,示例如下:
private void clearCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getSession().removeAttribute("cart");
response.sendRedirect("shop?action=showCart");
}
然後,在StoreServlet的switch中再新增一個case,示例如下:
case "clearCart":
clearCart(request,response);
break;
最後,在jsp中新增一個清除購物車的連結,示例如下:
<h2>
<a href="<c:url value="/shop?action=clearCart"/>">清空購物車</a>
</h2>
在HttpSession中有一個比較重要的方法,這就是invalidate(),當用戶登出時需要呼叫該方法,它將銷燬會話並解除所有繫結到會話的資料,即使瀏覽器使用相同的會話ID發起了另一個請求,已經無效的會話也不能再使用,它將會建立一個新的會話,然後響應新的會話給客戶端。
4、在會話中儲存更復雜一些的資料
首先,建立一個頁面請求相關的實體類,用於記錄頁面請求的相關資訊,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用於記錄頁面請求相關引數的實體類
*/
public class PageVisit implements Serializable{
private static final long serialVersionUID = 2470743552696749915L;
//頁面請求的訪問時間
private Long enteredTimestamp;
//頁面請求的結束時間
private Long lastTimestamp;
//請求的url和引數拼接字串
private String request;
//請求的遠端地址物件
private InetAddress ipAddress;
public Long getEnteredTimestamp() {
return enteredTimestamp;
}
public void setEnteredTimestamp(Long enteredTimestamp) {
this.enteredTimestamp = enteredTimestamp;
}
public Long getLastTimestamp() {
return lastTimestamp;
}
public void setLastTimestamp(Long lastTimestamp) {
this.lastTimestamp = lastTimestamp;
}
public String getRequest() {
return request;
}
public void setRequest(String request) {
this.request = request;
}
public InetAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(InetAddress ipAddress) {
this.ipAddress = ipAddress;
}
}
然後,建立一個處理會話活動的Servlet——ActivityServlet,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用於接收記錄或檢視session活動的請求
*/
@WebServlet(
name = "activityServlet",
urlPatterns = "/do/*"
)
public class ActivityServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
recordSessionActivity(req);
showSessionActivity(req,resp);
}
/**
* 請求的轉發
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
private void showSessionActivity(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{
request.getRequestDispatcher("/WEB-INF/jsp/showSessionActivity.jsp").forward(request,response);
}
/**
* 獲取到會話,如果會話中存在請求記錄就寫入最後一個請求記錄的頁面請求結束時間,之後向vector中新增新的請求記錄
* @param request
*/
private void recordSessionActivity(HttpServletRequest request){
HttpSession session = request.getSession();
if(null == session.getAttribute("activity")){
session.setAttribute("activity",new Vector<PageVisit>());
}
Vector<PageVisit> visits = (Vector<PageVisit>) session.getAttribute("activity");
if(!visits.isEmpty()){
PageVisit lastElement = visits.lastElement();
lastElement.setLastTimestamp(System.currentTimeMillis());
}
PageVisit pageVisit = new PageVisit();
pageVisit.setEnteredTimestamp(System.currentTimeMillis());
if(null == request.getQueryString()){
pageVisit.setRequest(request.getRequestURI());
}else {
//請求引數不為空時,將引數拼接到url中
pageVisit.setRequest(request.getRequestURI()+"?"+request.getQueryString());
}
String remoteAddr = request.getRemoteAddr();
try {
InetAddress inetAddress = InetAddress.getByName(remoteAddr);
pageVisit.setIpAddress(inetAddress);
} catch (UnknownHostException e) {
e.printStackTrace();
}
visits.add(pageVisit);
}
}
最後,編寫顯示會話活動記錄的Jsp——showSessionActivity.jsp,示例如下:
<%@ page import="java.text.DateFormat" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>
<%@ page import="java.util.Vector" %>
<%@ page import="f1.chapter5.pojo.PageVisit" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>請求記錄</title>
</head>
<body>
<%!
//宣告一個處理顯示時間的函式
private String showTime(long time){
String info = "";
if(time < 1000){
info = "小於1秒";
}else if(time < 60*1000){
info = time/1000 + "秒";
}else if(time < 60*60*1000){
info = time/(60*1000) + "分鐘";
}
return info;
}
%>
<%
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
%>
<h2>session屬性</h2>
session id :<%=session.getId()%><br>
session 是否未返回客戶端:<%=session.isNew()%><br>
session 建立時間:<%=format.format(new Date(session.getCreationTime()))%><br>
<h2>session中的頁面請求記錄</h2>
<%
Vector<PageVisit> visits = (Vector<PageVisit>) session.getAttribute("activity");
for(PageVisit visit:visits){
%>請求的url:<%=visit.getRequest()%><%
if(null != visit.getIpAddress()){
%> 發起請求的遠端IP地址:<%=visit.getIpAddress().getHostAddress()%><%
}
%> 請求訪問會話的時間:<%=format.format(new Date(visit.getEnteredTimestamp()))%><%
if(null != visit.getLastTimestamp()){
%> 請求停留時間:<%=showTime(visit.getLastTimestamp()-visit.getEnteredTimestamp())%><%
}
%><br><%
}
%>
</body>
</html>
該應用程式追蹤了所有的請求並在請求之間儲存了它們的資訊,最終顯示給使用者檢視。
四、使用會話
1、使用會話在應用程式中新增一個簡單的登入功能
首先,建立一個LoginServlet,用於控制使用者的登入與登出,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用於控制使用者的登入和登出
*/
@WebServlet(
name = "loginServlet",
urlPatterns = "/login"
)
public class LoginServlet extends HttpServlet{
//模擬一個靜態的、存在於記憶體之中的使用者資料庫
private static final Map<String,String> userMap = new Hashtable<>();
static {
userMap.put("zhangsan","zhangsan001");
userMap.put("lisi","lisi001");
userMap.put("wangwu","wangwu001");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
//如果是登出操作,則重定向到登入頁面
String logout = request.getParameter("logout");
if(null != logout && logout.equals("true")){
session.invalidate();
response.sendRedirect("login");
return;
}
//用於顯示登入頁面
if(null == session.getAttribute("username")){
//未登入的跳轉到登入頁面
request.setAttribute("loginFailed",false);
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,response);
}else {
//已經登入的跳轉到商品列表頁面
response.sendRedirect("shop");
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//用於驗證表單提交
HttpSession session = request.getSession();
if(null == session.getAttribute("username")){
//未登入要進行驗證
String username = request.getParameter("username");
String password = request.getParameter("password");
if(null == username || null == password || !userMap.containsKey(username) || !userMap.get(username).equals(password)){
//賬號或密碼驗證錯誤的將loginFailed置為true
request.setAttribute("loginFailed",true);
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,response);
}else {
session.setAttribute("username",username);
//登入成功後,改變sessionId
request.changeSessionId();
//跳轉到商品列表頁面
response.sendRedirect("shop");
}
}else {
//已經登入的跳轉到商品列表頁面
response.sendRedirect("shop");
}
}
}
然後,建立登入頁面login.jsp,用於提交使用者的登入賬號和密碼,示例如下:
<%@ page import="com.sun.org.apache.xpath.internal.operations.Bool" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>登入頁面</title>
</head>
<body>
<h2>登入</h2>
<br>
<%
Boolean loginFailed = (Boolean)request.getAttribute("loginFailed");
if(loginFailed){
out.print("賬號或密碼錯誤,請重新嘗試登入!");
}
%>
<br>
<form method="post" action="login">
賬號:<input type="text" name="username"><br>
密碼:<input type="text" name="password"><br>
<input type="submit" value="登入">
</form>
<br>
<h2>
<a href="login?logout=true">登出</a>
</h2>
</body>
</html>
最後,記得在browse.jsp頁面也新增一個登出的連結,示例如下:
<h2>
<a href="login?logout=true">登出</a>
</h2>
2、使用監聽器監測會話的變化
JavaEE中比較有用的特性之一就是會話事件,當會話發生變化時,Web容器將通知應用程式這些變化,該功能通過釋出訂閱模式實現,從而可以將修改會話和監聽會話變化的程式碼解耦。而用於檢測這些變化的工具被稱為監聽器。
現在在專案中建立一個SessionListener用於監聽會話的變化,它實現了HttpSessionListener和HttpSessionIdListener,示例如下:
/**
* author Alex
* date 2018/10/28
* description 用於監測會話狀態的變化
*/
@WebListener
public class SessionListener implements HttpSessionListener,HttpSessionIdListener{
/**
* 格式化時間
* @return
*/
private String showTime(){
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(new Date());
}
@Override
public void sessionIdChanged(HttpSessionEvent e, String oldSessionId) {
//當會話id改變時,新增會話活動日誌
System.out.println(showTime() + " 會話id已改變,oldSessionId=" + oldSessionId + ",newSessionId=" + e.getSession().getId());
}
@Override
public void sessionCreated(HttpSessionEvent e) {
//當會話建立時,新增會話活動日誌
System.out.println(showTime() + " 會話已建立,sessionId=" + e.getSession().getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent e) {
//當會話登出時,新增會話活動日誌
System.out.println(showTime() + " 會話已銷燬,銷燬的sessionId=" + e.getSession().getId());
}
}
關於監聽器的註冊,可以使用註解(比較簡潔),也可以在部署描述符中進行配置, 但兩者不可同時使用。在web.xml中配置listener的示例如下:
<!--在部署描述符中新增listener-->
<listener>
<listener-class>f1.chapter5.listener.SessionListener</listener-class>
</listener>
注意:在啟動了偵錯程式,但在開啟瀏覽器之前,高度視窗可能就已經出現了一條或多條日誌資訊,表示有一個或多個會話已經銷燬。這是完全正常的,當Tomcat關閉時,它將會把會話持久到檔案系統中,從而保證其中的資料不會丟失;當Tomcat重新啟動時,它會嘗試把這些序列化的會話恢復到記憶體中。如果持久化的會話過期了,那麼Tomcat將通知HttpSessionListener這些會話過期了,這在WebP容器中是很標準的做法。
3、維護活躍的會話列表
除了記錄會話活動,還可以在應用程式中維護一個活躍會話列表,下面來實現這個目標。首先,建立一個SessionRegistry類,使用該類模擬一個簡單的資料庫的增刪查改,作為會話登錄檔。示例如下:
**
* author Alex
* date 2018/10/28
* description 會話登錄檔
*/
public final class SessionRegistry {
private static final Map<String,HttpSession> SESSION_MAP = new Hashtable<>();
//將構造方法設定為私有,禁止建立該物件的例項
private SessionRegistry(){}
public static void addSession(HttpSession session){
SESSION_MAP.put(session.getId(),session);
}
public static void updateSession(HttpSession session,String oldSessionId){
//新增同步塊
synchronized (SESSION_MAP){
SESSION_MAP.remove(oldSessionId);
addSession(session);
}
}
public static void removeSession(HttpSession session){
SESSION_MAP.remove(session.getId());
}
public static List<HttpSession> getAllSession(){
return new ArrayList<>(SESSION_MAP.values());
}
public static int getNumberOfSession(){
return SESSION_MAP.size();
}
}
然後擴充套件一下SessionListener監聽器的實現方法,在sessionIdChanged方法中新增如下程式碼:
SessionRegistry.updateSession(e.getSession(),oldSessionId);
在sessionCreated方法中新增如下程式碼:
SessionRegistry.addSession(e.getSession());
在sessionDestroyed方法中新增如下程式碼:
SessionRegistry.removeSession(e.getSession());
然後再建立SessionListServlet,用於處理展示活躍會話列表的請求,示例如下:
/**
* author Alex
* date 2018/10/28
* description 處理展示活躍會話列表的請求
*/
@WebServlet(
name = "sessionListServlet",
urlPatterns = "/sessionList"
)
public class SessionListServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
if(null == session.getAttribute("username")){
response.sendRedirect("login");
return;
}
request.setAttribute("numberOfSession", SessionRegistry.getNumberOfSession());
request.setAttribute("sessionList",SessionRegistry.getAllSession());
request.getRequestDispatcher("/WEB-INF/jsp/sessionList.jsp").forward(request,response);
}
}
最後建立sessionList.jsp頁面,用於展示集合中的活躍會話列表,示例如下:
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Date" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>活躍的會話列表</title>
</head>
<body>
<h2>
<a href="login?logout=true">登出</a>
</h2>
<br>
<h2>活躍的會話列表</h2>
在這個應用程式中,現在總共有<%=request.getAttribute("numberOfSession")%>個會話存活
<br>
<%
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<HttpSession> sessionList = (List<HttpSession>) request.getAttribute("sessionList");
long nowTime = System.currentTimeMillis();
for(HttpSession session1:sessionList){
out.print("使用者名稱:" + session1.getAttribute("username"));
if(session1.getId().equals(session.getId())){
out.print("(你自己)");
}
out.print(" sessionId=" + session1.getId() + ",該會話最後訪問時間是:" + format.format(new Date(session1.getLastAccessedTime())));
out.print("<br>");
}
%>
</body>
</html>
五、將使用會話的應用程式叢集化
1、在叢集中使用會話ID
在叢集中使用會話會遇到的問題是:會話是以物件的形式存在於記憶體中,並且只存在於Web容器的單個例項中。在負載均衡的場景中,來自同一個客戶端的兩個連續請求將會訪問不同的Web容器,第一個Web容器將會為它收到的第一個會話請求分配會話ID,然後第二個請求將會由另一個Web容器例項進行處理,第二個例項無法識別其中的會話ID,因此將重新建立並分配一個新的會話ID,此時會話就變得無用了。
解決這個問題的方法是使用粘滯會話,粘滯會話的概念是:使負載均衡機制能感知到會話,並且總是將來自同一會話的請求傳送到相同的伺服器。比如在會話ID的末尾處新增一個Web伺服器的識別符號,用於在轉發請求時轉發到指定的Web伺服器。
2、會話複製和故障恢復
使用粘滯會話的問題是,它可以支援擴充套件性,但不支援高可用性。如果建立特定會話的Tomcat例項終止服務,那麼該會話將會丟失,並且使用者也需要重新登入。甚至於使用者可以丟失尚未儲存的工作。
因此,會話應該可以在叢集中複製,無論會話產生於哪個例項,它們對於所有的Web容器例項都是可用的。在應用程式中啟動會話複製是很簡單的,只需要在部署描述符中新增一個標籤即可,示例如下:
<distributable/>
這個標籤的存在就代表了Web容器將在叢集中複製會話,當會話在某個例項中建立時,它將被複制到其他例項中,如果會話特性發生了變化,該會話也會被重新複製到其他例項,使得它們一直擁有最新的會話資訊。