之前的學習總結:http://www.cnblogs.com/lyh421/p/6698871.html
1.kickout功能描述
如果將配置文件中的kickout設置為true,則在另處再次登錄時,會將第一次登錄的用戶踢出。

2.kickout的實現
2.1 新建KickoutSessionControlFilter extends AccessControlFilter
詳細的方法實現,后面再來完成。類存放於公共module:base_project中。
1 public class KickoutSessionControlFilter extends AccessControlFilter { 2 @Override 3 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { 4 return false; 5 } 6 7 @Override 8 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 9 return false; 10 } 11 }
2.2 配置spring-config-shiro.xml
這兩個文件配置在要使用kickout功能的module中。
(1)kickoutSessionControllerFilter
kickoutAfter:是否提出后來登錄的,默認為false,即后來登錄的踢出前者。
maxSession:同一個用戶的最大會話數,默認1,表示同一個用戶最多同時一個人登錄。
kickoutUrl:被踢出后重定向的地址。
1 <!--並發登錄控制--> 2 <bean id="kickoutSessionControlFilter" class="***.common.filter.KickoutSessionControlFilter"> 3 <property name="cacheManager" ref="springCacheManager"/> 4 <property name="kickoutAfter" value="false"/> 5 <property name="maxSession" value="1"/> 6 <property name="kickoutUrl" value="/login.do"/> 7 </bean>
(2)shiroFilter
此處配置什么時候走kickout 攔截器,進行並發登錄控制。這里攔截所有.jsp和.do的路徑。
1 <bean id="AuthRequestFilter" class="com.baosight.aas.auth.filter.AuthRequestFilter"/> 2 <!-- Shiro主過濾器本身功能十分強大,其強大之處就在於它支持任何基於URL路徑表達式的、自定義的過濾器的執行 --> 3 <!-- Web應用中,Shiro可控制的Web請求必須經過Shiro主過濾器的攔截,Shiro對基於Spring的Web應用提供了完美的支持 --> 4 <bean id="shiroFilter" class="com.baosight.aas.auth.filter.factory.ClientShiroFilterFactoryBean"> //略 13 <property name="filters"> 14 <util:map> 15 <entry key="authc" value-ref="formAuthenticationFilter"/> //略 19 <entry key="kickout" value-ref="kickoutSessionControlFilter"/> 20 </util:map> 21 </property> 27 <property name="filterChainDefinitions"> 28 <value> //略48 /**/*.jsp = forceLogout,authc,kickout 49 /**/*.do = forceLogout,authc,kickout 50 /** = forceLogout,authc 51 </value> 52 </property> 53 </bean>
2.3 ehcache.xml
注意,其他module在配置shiro的時候,都是使用的公共module:base_project中的ehcache.xml文件。在此文件中加上一段:
這里的名稱shiro-kickout-session在后面的kickoutController里要用到。
1 <cache name="shiro-kickout-session" 2 eternal="false" 3 timeToIdleSeconds="3600" 4 timeToLiveSeconds="0" 5 overflowToDisk="false" 6 statistics="true"> 7 </cache>
2.4 實現KickoutSessionControlFilter
1 package com.baosight.common.filter; 2 3 import org.apache.shiro.cache.Cache; 4 import org.apache.shiro.cache.CacheManager; 5 import org.apache.shiro.session.Session; 6 import org.apache.shiro.subject.Subject; 7 import org.apache.shiro.web.filter.AccessControlFilter; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import org.springframework.beans.factory.annotation.Value; 11 12 import javax.servlet.ServletRequest; 13 import javax.servlet.ServletResponse; 14 import java.io.Serializable; 15 import java.util.Deque; 16 import java.util.LinkedList; 17 18 /** 19 * Created by liyuhui on 2017/4/12. 20 */ 21 public class KickoutSessionControlFilter extends AccessControlFilter{ 22 private static final Logger LOGGER = LoggerFactory.getLogger(KickoutSessionControlFilter.class); 23 24 @Value("${aas.kickout:false}") 25 private String kickout; 26 27 private String kickoutUrl; 28 private boolean kickoutAfter = false; 29 private int maxSession = 1; 31 private Cache<String, Deque<Session>> cache; 32 33 public void setKickoutUrl(String kickoutUrl) { 34 this.kickoutUrl = kickoutUrl; 35 } 36 37 public void setKickoutAfter(boolean kickoutAfter) { 38 this.kickoutAfter = kickoutAfter; 39 } 40 41 public void setMaxSession(int maxSession) { 42 this.maxSession = maxSession; 43 } 44 45 public void setCacheManager(CacheManager cacheManager) { 46 this.cache = cacheManager.getCache("shiro-kickout-session"); 47 } 48 49 @Override 50 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { 51 return false; 52 } 53 54 @Override 55 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 56 if(!"true".equals(kickout)){ 57 //如果不需要單用戶登錄的限制 58 return true; 59 } 60 61 Subject subject = getSubject(request, response); 62 if(!subject.isAuthenticated() && !subject.isRemembered()){ 63 //如果沒登錄,直接進行之后的流程 64 return true; 65 } 66 67 Session session = subject.getSession(); 68 Serializable sessionId = session.getId(); 69 70 String usernameTenant = (String)session.getAttribute("loginName"); 71 synchronized (this.cache) { 72 if(cache == null){ 73 throw new Exception("cache 為空"); 74 } 75 Deque<Session> deque = cache.get(usernameTenant); 76 if (deque == null) { 77 deque = new LinkedList<Session>(); 78 cache.put(usernameTenant, deque); 79 } 80 81 //如果隊列里沒有此sessionId,且用戶沒有被踢出;放入隊列 82 boolean whetherPutDeQue = true; 83 if (deque.isEmpty()) { 84 whetherPutDeQue = true; 85 } else { 86 for (Session sessionInqueue : deque) { 87 if (sessionId.equals(sessionInqueue.getId())) { 88 whetherPutDeQue = false; 89 break; 90 } 91 } 92 } 93 if (whetherPutDeQue) { 94 deque.push(session); 95 } 96 this.LOGGER.debug("logged user:" + usernameTenant + ", deque size = " + deque.size()); 97 this.LOGGER.debug("deque = " + deque); 98 99 //如果隊列里的sessionId數超出最大會話數,開始踢人 100 while (deque.size() > maxSession) { 101 Session kickoutSession = null; 102 if (kickoutAfter) { //如果踢出后者 103 kickoutSession = deque.removeFirst(); 104 this.LOGGER.debug("踢出后登錄的,被踢出的sessionId為: " + kickoutSession.getId()); 105 } else { //否則踢出前者 106 kickoutSession = deque.removeLast(); 107 this.LOGGER.debug("踢出先登錄的,被踢出的sessionId為: " + kickoutSession.getId()); 108 } 109 if (kickoutSession != null) { 110 kickoutSession.stop(); 111 } 112 } 113 } 114 return true; 115 } 116 }
3.遇到的錯誤和說明
3.1 共享session的問題
項目中,使用了共享session,出現了踢出失效的問題。(已解決)
解決辦法:原本的實現代碼使用的是標記屬性,現在改為直接stop該session。
之前的代碼:
1 if (kickoutSession != null) { 2 //設置會話的kickout屬性表示踢出了 3 kickoutSession.setAttribute(KICK_OUT, true); 4 }
之后的代碼:
1 if (kickoutSession != null) { 2 kickoutSession.stop(); 3 }