getActiveSessions() 方法,無法直接枚舉所有 Session。實作此介面的物件被放入/移出 Session 時,容器自動回調:
public interface HttpSessionBindingListener extends EventListener {
void valueBound(HttpSessionBindingEvent event); // session.setAttribute() 時
void valueUnbound(HttpSessionBindingEvent event); // session.removeAttribute()/逾時/失效時
}
關鍵優勢: 即使用戶未點登出,Session 逾時/失效時容器保證觸發 valueUnbound,實現精準清理。
public class OnlineUserTracker implements HttpSessionBindingListener {
private ServletContext application;
private static final String ACTIVE_SESSIONS_KEY = "activeSessions";
public OnlineUserTracker(ServletContext application) {
if (application == null) throw new IllegalArgumentException("Application context required");
this.application = application;
}
@Override
public void valueBound(HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
// 1. 獲取或建立全域 Session 列表
@SuppressWarnings("unchecked")
List activeSessions = (List) application.getAttribute(ACTIVE_SESSIONS_KEY);
if (activeSessions == null) {
activeSessions = new CopyOnWriteArrayList<>(); // 線程安全
application.setAttribute(ACTIVE_SESSIONS_KEY, activeSessions);
}
// 2. 驗證用戶身份 (假設登入後存放 User 物件)
User user = (User) session.getAttribute("user");
if (user != null) {
activeSessions.add(session); // 記錄 Session 而非 User,支援踢人、強制下線
}
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
@SuppressWarnings("unchecked")
List activeSessions = (List) application.getAttribute(ACTIVE_SESSIONS_KEY);
if (activeSessions != null) {
activeSessions.remove(session); // 精準移除
}
}
}
public void login(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = userService.authenticate(username, password);
if (user == null) {
req.setAttribute("error", "帳號或密碼錯誤");
req.getRequestDispatcher("/login.jsp").forward(req, resp);
return;
}
HttpSession session = req.getSession(true);
session.setAttribute("user", user); // 業務用戶物件
session.setAttribute("tracker", new OnlineUserTracker(getServletContext())); // 監控器
resp.sendRedirect("/dashboard.jsp");
}
public void logout(HttpServletRequest req, HttpServletResponse resp) {
HttpSession session = req.getSession(false);
if (session != null) {
// 1. 從全域列表移除 (valueUnbound 會被觸發,但主動移除更即時)
ServletContext app = getServletContext();
@SuppressWarnings("unchecked")
List activeSessions = (List) app.getAttribute("activeSessions");
if (activeSessions != null) {
activeSessions.remove(session);
}
// 2. 清理 Session 所有屬性
Enumeration names = session.getAttributeNames();
while (names.hasMoreElements()) {
session.removeAttribute(names.nextElement());
}
// 3. 記錄登出時間、審計日誌
userService.recordLogout((User) session.getAttribute("user"));
session.invalidate();
}
resp.sendRedirect("/login.jsp");
}
<%@ page import="java.util.*, javax.servlet.http.*" %>
<%
List activeSessions = (List) application.getAttribute("activeSessions");
%>
<h2>當前在線用戶 (共 <%= activeSessions != null ? activeSessions.size() : 0 %> 人)</h2>
<table border="1">
<tr><th>用戶名</th><th>登入時間</th><th>IP</th><th>操作</th></tr>
<%
if (activeSessions != null) {
for (HttpSession s : activeSessions) {
User u = (User) s.getAttribute("user");
if (u != null) {
%>
<tr>
<td><%= u.getUsername() %></td>
<td><%= new Date(s.getCreationTime()) %></td>
<td><%= s.getAttribute("clientIP") %></td>
<td><a href="admin/kick.jsp?sessionId=<%= s.getId() %>">強制下線</a></td>
</tr>
<%
}
}
%>
</table>
| 議題 | 解決方案 |
|---|---|
| 叢集環境 Session 共享 | 使用 Redis/Memcached 集中存儲 Session,activeSessions 亦存入 Redis Set,所有節點讀寫同一數據源 |
| 大量用戶下 activeSessions 列表效能 | 分頁顯示、緩存統計數字、異步更新前端 |
| 防止 Session Fixation 攻擊 | 登入成功後 session.invalidate() 再 req.getSession(true) 建立新 Session |
| 「發呆」用戶判定 (iframe 保活場景) | 引入「最後活躍時間」屬性,定時任務掃描超過閾值 (如 30 分鐘) 的 Session 主動 invalidate |
| 監控器物件序列化問題 (叢集) | OnlineUserTracker 不實作 Serializable,或標記 transient,避免跨節點序列化失敗 |
HttpSessionBindingListener 是 Servlet 規範提供的「隱形鉤子」,無需輪詢、無需修改容器代碼,即可實現精準、即時、自動清理的在線用戶統計。核心模式:登入雙綁定 (User + Tracker)、逾時自動回調清理、Application 域共享列表、管理端讀取列表渲染。此模式同樣適用於購物車商品數統計、文檔協作在線者顯示、即時聊天室成員管理等場景。