首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Session 監聽器實現同賬號單點登入與並發人數限制

     編輯:Java 相關知識

前言

在企業級應用中,常見兩大會話管控需求:

  1. 同賬號單點登入:同一賬號同一時間只允許一處登入,後登入踢掉前登入(或阻止後登入)。
  2. 全局並發人數限制:超過授權用戶數拒絕新登入,提示「系統繁忙」。

本文基於 HttpSessionAttributeListener 實現一個零侵入、集中式、線程安全的會話管控組件,核心邏輯集中在監聽器,Action 僅需讀取結果碼。

核心設計

數據結構

// 保存「用戶名 ↔ Session」映射
static class SessionAndUser {
    String userName;
    HttpSession session;
    String sid;  // session.getId()
}

全局靜態列表:

private static final List SESSIONS = 
        Collections.synchronizedList(new ArrayList<>());

監聽器完整實現

package com.example.listener;

import javax.servlet.http.*;
import java.util.*;
import com.opensymphony.xwork2.ActionContext;

public class OnlineListener implements HttpSessionListener, HttpSessionAttributeListener {

    // 輔助標記,處理 attributeReplaced → attributeRemoved 串聯
    private static int pendingRemoveIndex = -1;
    private static boolean replaceTriggered = false;

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        // 可選:記錄創建日誌
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // 會話銷毀(超時/手動 invalidate)時,從列表移除
        HttpSession session = se.getSession();
        String sid = session.getId();
        synchronized (SESSIONS) {
            SESSIONS.removeIf(sau -> sau.sid.equals(sid));
        }
    }

    // === 核心:屬性新增(登入時 setAttribute("username", ...))===
    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        if (!"username".equals(event.getName())) return;

        HttpSession session = event.getSession();
        String username = (String) event.getValue();
        ActionContext ctx = ActionContext.getContext();

        synchronized (SESSIONS) {
            // 1. 並發人數限制
            if (SESSIONS.size() >= Constant.USER_LIMIT) {
                ctx.put("timeoutMSG", "serverBusy");
                return;
            }

            // 2. 同賬號檢查:已登入則標記
            for (SessionAndUser sau : SESSIONS) {
                if (sau.userName.equals(username)) {
                    ctx.put("timeoutMSG", "beenLoged");
                    return;
                }
            }

            // 3. 新登入記錄
            SessionAndUser sau = new SessionAndUser();
            sau.userName = username;
            sau.session = session;
            sau.sid = session.getId();
            SESSIONS.add(sau);
        }
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        if (!"username".equals(event.getName())) return;

        // 正常登出/超時觸發:直接移除
        if (pendingRemoveIndex == -1) {
            String username = (String) event.getValue();
            synchronized (SESSIONS) {
                SESSIONS.removeIf(sau -> sau.userName.equals(username));
            }
        } else {
            // 由 attributeReplaced 觸發的踢人流程:延遲移除
            if (replaceTriggered) {
                synchronized (SESSIONS) {
                    SESSIONS.remove(pendingRemoveIndex);
                }
                pendingRemoveIndex = -1;
                replaceTriggered = false;
            }
        }
    }

    // === 關鍵:屬性替換(同賬號在另一處登入,觸發替換)===
    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {
        if (!"username".equals(event.getName())) return;

        HttpSession newSession = event.getSession();
        String newUsername = (String) newSession.getAttribute("username"); // 新值
        String oldUsername = (String) event.getValue();                    // 舊值

        synchronized (SESSIONS) {
            for (int i = SESSIONS.size() - 1; i >= 0; i--) {
                SessionAndUser sau = SESSIONS.get(i);
                // 情況 A:同用戶名在不同 Session 登入 → 踢掉舊 Session
                if (sau.userName.equals(newUsername) && !sau.sid.equals(newSession.getId())) {
                    pendingRemoveIndex = i;
                    replaceTriggered = true;
                    sau.session.invalidate();  // 觸發 attributeRemoved → 移除列表
                    break;
                }
                // 情況 B:同 Session 內用戶名變更 → 更新記錄
                if (sau.sid.equals(newSession.getId())) {
                    sau.userName = newUsername;
                }
            }
        }
    }
}

常量定義

public class Constant {
    public static final String USER_NAME = "username";
    public static final int USER_LIMIT = 100;  // 最大並發數,可從配置讀取
}

web.xml 註冊


    com.example.listener.OnlineListener

Action 層處理

public class LoginAction extends ActionSupport {
    // ... 登入校驗邏輯 ...

    // 登入成功後設置 username 到 Session,觸發監聽器
    session.setAttribute(Constant.USER_NAME, operator.getUsername());

    // 讀取監聽器寫入的結果碼
    ActionContext ctx = ActionContext.getContext();
    if ("serverBusy".equals(ctx.get("timeoutMSG"))) {
        ctx.put("timeoutMSG", "系統繁忙,請稍後再試");
        return "login";
    }
    if ("beenLoged".equals(ctx.get("timeoutMSG"))) {
        ctx.put("timeoutMSG", "該賬號已在別處登入");
        return "login";
    }
    return "success";
}

頁面提示

<%@ taglib prefix="s" uri="/struts-tags"%>

關鍵邏輯圖解


[用戶 A 登入] 
    → session.setAttribute("username", "A")
    → attributeAdded: 列表無 A → 添加 (A, Session1)

[用戶 A 在另一瀏覽器登入]
    → session.setAttribute("username", "A")  (Session2)
    → attributeReplaced: 發現列表有 A 且 sid 不同
        → 標記 pendingRemoveIndex, replaceTriggered=true
        → Session1.invalidate()
    → attributeRemoved (由 Session1 銷毀觸發)
        → pendingRemoveIndex != -1 且 replaceTriggered
        → 移除列表索引 pendingRemoveIndex (即 Session1 記錄)
    → attributeAdded (Session2)
        → 列表無 A (已被移除) → 添加 (A, Session2)
結果:Session1 被踢下線,Session2 成為唯一合法會話

併發安全說明

  • Collections.synchronizedList 保證列表操作原子性。
  • 關鍵區段(檢查+添加/移除)使用 synchronized (SESSIONS) 進一步保證複合操作原子性。
  • pendingRemoveIndex + replaceTriggered 雙標記解決 attributeReplaced → session.invalidate() → attributeRemoved 異步串聯問題。

擴展建議

  1. 集群環境:單機靜態列表不共享。需改用 Redis 存儲 user:sessions:{username} → Set,配合 Spring Session 實現分布式會話管控。
  2. 踢人策略可配置:支持「阻止新登入」或「踢掉舊登入」,通過配置項切換邏輯分支。
  3. 審計日誌:記錄登入/踢人/超時事件,包含 IP、User-Agent、時間戳。
  4. 優雅下線通知:被踢的 Session 在下次請求時(或通過 WebSocket/長輪詢)提示「您已在別處登入」。
  5. 總結

    利用 HttpSessionAttributeListener 監聽 username 屬性的增刪改,配合靜態同步列表維護「用戶↔Session」映射,可在零侵入業務代碼的前提下實現:

    • 同賬號單點登入(踢人模式)
    • 全局並發人數限制
    • Session 銷毀自動清理

    此方案適用於單機/Session 不共享的傳統 Web 應用;微服務/集群環境請遷移至 Spring Session + Redis 方案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved