在認證階段(Realm.doGetAuthenticationInfo)掃描所有活躍 Session,若發現同用戶名的歷史 Session,則強制刪除(踢下線),實現「後登錄者勝出,前登錄者失效」的單點登錄語義。**無需集成 Spring**,僅使用 Shiro 核心 API 與 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
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();
}
}
| 組件 | 作用 |
|---|---|
MemorySessionDAO | 內存存儲 Session,集群環境需替換為 RedisSessionDAO / JdbcSessionDAO |
DefaultWebSessionManager | 管理 Session 生命週期,提供 getActiveSessions() 掃描接口 |
PRINCIPALS_SESSION_KEY | Shiro 內部鍵,Session 中存儲當前登錄用戶的 PrincipalCollection |
sessionDAO.delete(session) | 刪除 Session,下次該 Session 發起請求時會拋出 UnknownSessionException,Subject 判定為未認證 |
被踢用戶下次請求時,Shiro 過濾器鏈檢測到 Session 無效/不存在,會重定向至 authc.loginUrl。前端可通過攔截器/過濾器檢測響應狀態碼或特定標識,彈出「您的賬號在其他設備登錄,請重新登錄」提示。
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() 掃描全集群會話,實現分佈式單點登錄。
高併發登錄時,多線程同時執行踢人邏輯可能導致 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 反向索引)。