第十八章 併發登入人數控制——《跟我學Shiro》
在某些專案中可能會遇到如每個賬戶同時只能有一個人登入或幾個人同時登入,如果同時有多人登入:要麼不讓後者登入;要麼踢出前者登入(強制退出)。比如spring security就直接提供了相應的功能;Shiro的話沒有提供預設實現,不過可以很容易的在Shiro中加入這個功能。
示例程式碼基於《第十六章 綜合例項》完成,通過Shiro Filter機制擴充套件KickoutSessionControlFilter完成。
首先來看看如何配置使用(spring-config-shiro.xml)
kickoutSessionControlFilter用於控制併發登入人數的
Java程式碼- <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配置
Java程式碼- <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專案開發腳手架,其提供了後臺踢出使用者的功能: