在某些項目中可能會遇到如每個賬戶同時只能有一個人登錄或幾個人同時登錄,如果同時有多人登錄:要么不讓后者登錄;要么踢出前者登錄(強制退出)。比如 spring security 就直接提供了相應的功能;Shiro 的話沒有提供默認實現,不過可以很容易的在 Shiro 中加入這個功能。
示例代碼基于《第十六章 綜合實例》完成,通過 Shiro Filter 機制擴展 KickoutSessionControlFilter 完成。
**首先來看看如何配置使用(spring-config-shiro.xml)**
kickoutSessionControlFilter 用于控制并發登錄人數的
``` xml
<bean id="kickoutSessionControlFilter"
class="com.github.zhangkaitao.shiro.chapter18.web.shiro.filter.KickoutSessionControlFilter">
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="kickoutAfter" value="false"/>
<property name="maxSession" value="2"/>
<property name="kickoutUrl" value="/login?kickout=1"/>
</bean>
```
* cacheManager:使用 cacheManager 獲取相應的 cache 來緩存用戶登錄的會話;用于保存用戶—會話之間的關系的;
* sessionManager:用于根據會話 ID,獲取會話進行踢出操作的;
* kickoutAfter:是否踢出后來登錄的,默認是 false;即后者登錄的用戶踢出前者登錄的用戶;
* maxSession:同一個用戶最大的會話數,默認 1;比如 2 的意思是同一個用戶允許最多同時兩個人登錄;
* kickoutUrl:被踢出后重定向到的地址;
shiroFilter 配置
``` xml
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="filters">
<util:map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
<entry key="sysUser" value-ref="sysUserFilter"/>
<entry key="kickout" value-ref="kickoutSessionControlFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/login = authc
/logout = logout
/authenticated = authc
/** = kickout,user,sysUser
</value>
</property>
</bean>
```
此處配置除了登錄等之外的地址都走 kickout 攔截器進行并發登錄控制。
**測試**
此處因為 maxSession=2,所以需要打開 3 個瀏覽器(需要不同的瀏覽器,如 IE、Chrome、Firefox),分別訪問 `http://localhost:8080/chapter18/` 進行登錄;然后刷新第一次打開的瀏覽器,將會被強制退出,如顯示下圖:

KickoutSessionControlFilter 核心代碼:
``` java
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果沒有登錄,直接進行之后的流程
return true;
}
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
//TODO 同步控制
Deque<Serializable> deque = cache.get(username);
if(deque == null) {
deque = new LinkedList<Serializable>();
cache.put(username, deque);
}
//如果隊列里沒有此sessionId,且用戶沒有被踢出;放入隊列
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
//如果隊列里的sessionId數超出最大會話數,開始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { //否則踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession =
sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//設置會話的kickout屬性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
//會話被踢出了
try {
subject.logout();
} catch (Exception e) { //ignore
}
saveRequest(request);
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
```
此處使用了 Cache 緩存用戶名—會話 id 之間的關系;如果量比較大可以考慮如持久化到數據庫 / 其他帶持久化的 Cache 中;另外此處沒有并發控制的同步實現,可以考慮根據用戶名獲取鎖來控制,減少鎖的粒度。
另外可參考 JavaEE 項目開發腳手架,其提供了后臺踢出用戶的功能:
[](https://github.com/zhangkaitao/es/blob/master/web/src/main/java/com/sishuok/es/sys/user/web/controller/UserOnlineController.java)<https://github.com/zhangkaitao/es/blob/master/web/src/main/java/com/sishuok/es/sys/user/web/controller/UserOnlineController.java>
- 第1章 Shiro簡介
- 1.1 簡介
- 第2章 身份驗證
- 第3章 授權
- 第4章 INI配置
- 第5章 編碼 / 加密
- 第6章 Realm及相關對象
- 第7章 與Web集成
- 第8章 攔截器機制
- 第9章 JSP標簽
- 第10章 會話管理
- 第11章 緩存機制
- 第12章 與Spring集成
- 第13章 RememberMe
- 第14章 SSL
- 第15章 單點登錄
- 第16章 綜合實例
- 第17章 OAuth2集成
- 第18章 并發登錄人數控制
- 第19章 動態URL權限控制
- 第20章 無狀態Web應用集成
- 第21章 授予身份及切換身份
- 第22章 集成驗證碼
- 第23章 多項目集中權限管理及分布式會話
- 第24章 在線會話管理