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

Shiro 實現單用戶登錄(踢下線)機制:基於 Session 管理的強制下線最佳實踐

     編輯:Java 綜合教學

Shiro 實現單用戶登錄(踢下線)機制:基於 Session 管理的強制下線最佳實踐

「同一賬號同一時間僅允許一處登錄」是眾多系統的安全合規要求。Apache Shiro 通過 SessionManager + SessionDAO 提供會話管理能力,結合自定義 Realm 在認證階段清理舊會話,即可實現優雅的強制下線。本文基於 Shiro 1.4+(非 Spring 整合模式,使用 shiro.ini 配置)完整演示實現方案。

一、核心原理

  1. 用戶提交登錄請求 → Subject.login(token)
  2. SecurityManager 委託 Realm.doGetAuthenticationInfo() 執行認證
  3. 關鍵點:在認證方法執行前,遍歷所有活躍 Session,發現同用戶名的舊 Session → 刪除
  4. 舊 Session 被刪除後,舊端下次請求時 Shiro 檢測到 Session 不存在 → 強制重定向到登錄頁
  5. 新登錄認證通過,創建新 Session,完成踢換

二、shiro.ini 完整配置

[main]
# 登錄成功/失敗跳轉
authc.loginUrl = /login.jsp
authc.successUrl = /web/index.jsp

# 緩存管理器(內存,生產建議 Redis)
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager

# SecurityManager
securityManager = org.apache.shiro.web.mgt.DefaultWebSecurityManager
securityManager.cacheManager = $cacheManager
securityManager.sessionManager = $sessionManager
securityManager.realms = $localAuthorizingRealm

# Session 管理器(必須配置,支持強制下線)
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
sessionManager.sessionDAO = $sessionDAO
sessionManager.globalSessionTimeout = 1800000  # 30分鐘

# SessionDAO(內存實現,集群需換 Redis/JDBC 實現)
sessionDAO = org.apache.shiro.session.mgt.eis.MemorySessionDAO

# 自定義 Realm(實現踢下線邏輯)
localAuthorizingRealm = com.example.shiro.LocalAuthorizingRealm
# localAuthorizingRealm.credentialsMatcher = $credentialsMatcher

三、自定義 Realm 實現踢下線邏輯

package com.example.shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.subject.support.DefaultSubjectContext;

import java.util.Collection;

public class LocalAuthorizingRealm extends AuthorizingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        
        // ========== 核心:踢下線邏輯 ==========
        // 1. 獲取 SecurityManager
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) 
            org.apache.shiro.SecurityUtils.getSecurityManager();
        
        // 2. 獲取 SessionManager
        DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) 
            securityManager.getSessionManager();
        
        // 3. 獲取所有活躍 Session
        SessionDAO sessionDAO = sessionManager.getSessionDAO();
        Collection activeSessions = sessionDAO.getActiveSessions();
        
        if (CollectionUtils.isEmpty(activeSessions)) {
            return doAuthenticate(username, token);  // 無活躍會話,直接認證
        }

        // 4. 遍歷查找並刪除同用戶的舊 Session
        for (Session session : activeSessions) {
            // Session 中存儲的主體標識鍵
            Object principalObj = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (principalObj instanceof PrincipalCollection) {
                PrincipalCollection principals = (PrincipalCollection) principalObj;
                String existingUsername = (String) principals.getPrimaryPrincipal();
                
                if (username.equals(existingUsername)) {
                    // 發現同用戶舊會話 → 刪除(強制下線)
                    sessionDAO.delete(session);
                    System.out.println("踢下線:用戶 [" + username + "] 舊會話 " + session.getId() + " 已刪除");
                }
            }
        }
        // ======================================

        return doAuthenticate(username, token);
    }

    private AuthenticationInfo doAuthenticate(String username, AuthenticationToken token) {
        // 實際項目中:查詢數據庫驗證密碼、鎖定狀態等
        // 此處簡化:假設密碼驗證通過
        String passwordFromDb = "123456";  // 模擬從 DB 獲取
        return new SimpleAuthenticationInfo(username, passwordFromDb, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 權限授權邏輯(角色/權限查詢)
        return null;
    }
}

四、關鍵類與方法對照表

組件 作用 關鍵方法
DefaultWebSecurityManager Web 環境安全管理器 getSessionManager()
DefaultWebSessionManager Web 會話管理器 getSessionDAO(), getActiveSessions()
SessionDAO 會話持久化/查詢接口 getActiveSessions(), delete(Session)
MemorySessionDAO 內存實現(單機) 集群需替換為 RedisSessionDAO/JdbcSessionDAO
DefaultSubjectContext.PRINCIPALS_SESSION_KEY Session 中存儲主體標識的鍵常量 值為 shiroPrincipalCollection

五、客戶端體驗與交互流程

  1. 用戶 A 在瀏覽器 1 登錄 → 創建 Session S1
  2. 用戶 A 在瀏覽器 2 登錄 → 認證前掃描到 S1 → 刪除 S1
  3. 瀏覽器 1 發起請求 → Shiro 過濾器檢測 Session S1 不存在 → 重定向到 login.jsp
  4. 瀏覽器 2 登錄成功 → 創建新 Session S2

六、生產環境增強建議

🔧 集群環境必須替換 SessionDAO

  • RedisSessionDAO (shiro-redis):高性能、支持 Session 共享、集群踢下線
  • JdbcSessionDAO:數據庫持久化,事務一致性強
  • 配置示例:
    # shiro-redis 示例
    sessionDAO = org.crazycake.shiro.RedisSessionDAO
    sessionDAO.redisManager = $redisManager
    sessionDAO.expire = 1800

⚡ 性能優化:避免全量掃描

  • 當前方案:getActiveSessions() 遍歷所有會話,用戶量大時性能差
  • 優化方案:維護 Map<String, SessionId> usernameToSessionId(Redis Hash 或 ConcurrentHashMap)
  • 登錄時直接按用戶名查 SessionId → 精準刪除,O(1) 複雜度

🔔 用戶體驗優化:主動通知被踢端

  • WebSocket / Server-Sent Events / 長輪詢推送「您在別處登錄,已被迫下線」
  • 前端收到通知 → 彈窗提示 → 清理本地 Token → 跳轉登錄頁
  • 避免用戶操作到一半突然失效無感知

七、常見問題排查

  • 換用 RedisSessionDAO
  • 確認 sessionManager 配置生效
  • 檢查 Realm 認證成功後是否正確存入 Principal
  • 調試輸出 session.getAttributeKeys()
  • 多線程同時執行踢下線邏輯
  • 加分布式鎖(Redis SETNX)或單線程串行化
  • RememberMe 機制獨立於 Session
  • 踢下線時同時清除 RememberMe Cookie,或禁用 RememberMe
  • 現象 原因 解決
    踢下線不生效,舊端仍可操作 SessionDAO 非集群實現,多節點會話不同步
    刪除 Session 後報 NullPointerException Session 屬性鍵不匹配,PRINCIPALS_SESSION_KEY 為空
    並發登錄導致數據不一致
    RememberMe 導致自動登錄繞過踢下線

    八、總結

    1. 核心機制:認證前掃描活躍 Session → 按用戶名匹配 → 刪除舊 Session
    2. 關鍵配置:sessionManager + sessionDAO 必須在 securityManager 中注入
    3. 集群必備:MemorySessionDAO 僅適合單機,生產必換 RedisSessionDAO
    4. 性能優化:維護用戶名→SessionId 映射,避免全量掃描
    5. 體驗升級:WebSocket 主動推送下線通知,實現「感知式」踢下線

    單用戶登錄不僅是安全合規,更是會話管理能力的綜合體現。掌握 Shiro Session 體系,可輕鬆擴展會話審計、異地登錄預警、設備指紋綁定等高級特性。

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