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

Apache Shiro 實現單點登錄(同一用戶同一時間僅允許一處登錄)方案

     編輯:Java 相關知識

方案原理

在認證階段(Realm.doGetAuthenticationInfo)掃描所有活躍 Session,若發現同用戶名的歷史 Session,則強制刪除(踢下線),實現「後登錄者勝出,前登錄者失效」的單點登錄語義。**無需集成 Spring**,僅使用 Shiro 核心 API 與 shiro.ini 配置。

1. shiro.ini 核心配置

[main]
# 登錄/成功 URL
authc.loginUrl = /login.jsp
authc.successUrl = /web/index.jsp

# 緩存管理器
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager

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

# Session 管理:必須配置 SessionDAO 支持會話查詢/刪除
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
sessionManager.sessionDAO = $sessionDAO
sessionDAO = org.apache.shiro.session.mgt.eis.MemorySessionDAO

# 自定義 Realm
localAuthorizingRealm = com.example.shiro.LocalAuthorizingRealm
# 如有 LDAP/JDBC Realm 可追加
# securityManager.realms = $ldapRealm, $localAuthorizingRealm

2. 自定義 Realm:踢人邏輯

public class LocalAuthorizingRealm extends AuthorizingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        String username = (String) token.getPrincipal();
        
        // 1. 獲取 SecurityManager 並轉型為 DefaultWebSecurityManager
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) 
            SecurityUtils.getSecurityManager();
        
        // 2. 獲取 SessionManager
        DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) 
            securityManager.getSessionManager();
        
        // 3. 獲取所有活躍 Session
        Collection activeSessions = sessionManager.getSessionDAO().getActiveSessions();
        
        // 4. 遍歷查找同用戶名 Session 並刪除
        for (Session session : activeSessions) {
            // Session 中存儲的 Principal 集合(登錄時 set 進去的)
            PrincipalCollection principals = (PrincipalCollection) 
                session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            
            if (principals != null) {
                String existingUser = (String) principals.getPrimaryPrincipal();
                if (username.equals(existingUser)) {
                    // 找到歷史登錄會話,強制刪除(踢下線)
                    sessionManager.getSessionDAO().delete(session);
                    // 可選:記錄踢人日誌
                    log.info("用戶 {} 在新設備登錄,踢除舊會話: {}", username, session.getId());
                }
            }
        }
        
        // 5. 正常認證流程(此處簡化,實際需查詢數據庫驗證密碼)
        // 假設從數據庫獲取鹽值密碼
        String dbPassword = userService.findPasswordByUsername(username);
        if (dbPassword == null) throw new UnknownAccountException();
        
        return new SimpleAuthenticationInfo(username, dbPassword, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 權限授權邏輯...
        return new SimpleAuthorizationInfo();
    }
}

3. 關鍵點解析

組件作用
MemorySessionDAO內存存儲 Session,集群環境需替換為 RedisSessionDAO / JdbcSessionDAO
DefaultWebSessionManager管理 Session 生命週期,提供 getActiveSessions() 掃描接口
PRINCIPALS_SESSION_KEYShiro 內部鍵,Session 中存儲當前登錄用戶的 PrincipalCollection
sessionDAO.delete(session)刪除 Session,下次該 Session 發起請求時會拋出 UnknownSessionException,Subject 判定為未認證

4. 客戶端感知與處理

被踢用戶下次請求時,Shiro 過濾器鏈檢測到 Session 無效/不存在,會重定向至 authc.loginUrl。前端可通過攔截器/過濾器檢測響應狀態碼或特定標識,彈出「您的賬號在其他設備登錄,請重新登錄」提示。

5. 集群環境擴展:RedisSessionDAO


sessionDAO = org.apache.shiro.session.mgt.eis.RedisSessionDAO
sessionDAO.redisManager = $redisManager
redisManager = org.apache.shiro.session.mgt.eis.RedisManager
redisManager.host = localhost
redisManager.port = 6379
redisManager.timeout = 2000
redisManager.password = your_password



    org.crazycake
    shiro-redis
    3.3.0

RedisSessionDAO 將 Session 序列化存入 Redis,多節點共享 Session 狀態,getActiveSessions() 掃描全集群會話,實現分佈式單點登錄。

6. 進階:同步並發控制

高併發登錄時,多線程同時執行踢人邏輯可能導致 Session 狀態不一致。建議:

// 使用分佈式鎖(Redisson)保護踢人邏輯
RLock lock = redisson.getLock("shiro:kick:" + username);
lock.lock();
try {
    // 掃描並刪除舊 Session
} finally {
    lock.unlock();
}

總結

Shiro 單點登錄核心:**認證前掃描活躍 Session → 按 Principal 匹配 → 刪除舊 Session**。配置 MemorySessionDAO(單機)或 RedisSessionDAO(集群),自定義 Realm 實現踢人邏輯。優點:無侵入業務代碼、配置簡單、支援集群擴展。缺點:掃描全量 Session 性能隨用戶數線性增長,百萬級用戶需優化(如維護 username → Set 反向索引)。

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