原博客地址:http://jinnianshilongnian.iteye.com/blog/2018398
根據下載的pdf學習。
開濤shiro教程-第十八章-並發登錄人數控制
shiro中沒有提供默認實現,不過可以很容易實現。通過shiro filter機制拓展KickoutSessionControllerFilter。
kickoutSessionControllerFilter -> 將這個filter配置到shiro的配置文件中 -> 遇到的一些問題。
示例代碼的結構:
1.配置spring-config-shiro.xml
(1)kickoutSessionControllerFilter
kickoutAfter:是否提出后來登錄的,默認為false,即后來登錄的踢出前者。
maxSession:同一個用戶的最大會話數,默認1,表示同一個用戶最多同時一個人登錄。
kickoutUrl:被踢出后重定向的地址。
1 <bean id="kickoutSessionControlFilter" 2 class="com.github.zhangkaitao.shiro.chapter18.web.shiro.filter.KickoutSessionControlFilter"> 3 <property name="cacheManager" ref="cacheManager"/> 4 <property name="sessionManager" ref="sessionManager"/> 5 <property name="kickoutAfter" value="false"/> 6 <property name="maxSession" value="2"/> 7 <property name="kickoutUrl" value="/login?kickout=1"/> 8 </bean>
(2)shiroFilter
此處配置除了登錄等之外的地址都走 kickout 攔截器進行並發登錄控制。
1 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> 2 <property name="securityManager" ref="securityManager"/> 3 <property name="loginUrl" value="/login"/> 4 <property name="filters"> 5 <util:map> 6 <entry key="authc" value-ref="formAuthenticationFilter"/> 7 <entry key="sysUser" value-ref="sysUserFilter"/> 8 <entry key="kickout" value-ref="kickoutSessionControlFilter"/> 9 </util:map> 10 </property> 11 <property name="filterChainDefinitions"> 12 <value> 13 /login = authc 14 /logout = logout 15 /authenticated = authc 16 /** = kickout,user,sysUser 17 </value> 18 </property> 19 </bean>
(3) ehcache.xml
這里的名稱在后面的kickoutController里要用到。
1 <cache name="shiro-kickout-session" 2 maxEntriesLocalHeap="2000" 3 eternal="false" 4 timeToIdleSeconds="3600" 5 timeToLiveSeconds="0" 6 overflowToDisk="false" 7 statistics="true"> 8 </cache>
2.KickoutSessionControllerFilter
此處,使用了Cache緩存"用戶名-會話id"之間的關系,如果量比較大的話,可以考慮持久化到數據庫/其他持久化的Cache中。
另外,此處沒有並發控制的同步實現,可以考慮根據用戶名來獲取鎖,減少鎖的粒度。
1 package com.github.zhangkaitao.shiro.chapter18.web.shiro.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.session.mgt.DefaultSessionKey; 7 import org.apache.shiro.session.mgt.SessionManager; 8 import org.apache.shiro.subject.Subject; 9 import org.apache.shiro.web.filter.AccessControlFilter; 10 import org.apache.shiro.web.util.WebUtils; 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 * <p>User: Zhang Kaitao 20 * <p>Date: 14-2-18 21 * <p>Version: 1.0 22 */ 23 public class KickoutSessionControlFilter extends AccessControlFilter { 24 25 private String kickoutUrl; //踢出后到的地址 26 private boolean kickoutAfter = false; //踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶 27 private int maxSession = 1; //同一個帳號最大會話數 默認1 28 29 private SessionManager sessionManager; 30 private Cache<String, Deque<Serializable>> cache; 31 32 public void setKickoutUrl(String kickoutUrl) { 33 this.kickoutUrl = kickoutUrl; 34 } 35 36 public void setKickoutAfter(boolean kickoutAfter) { 37 this.kickoutAfter = kickoutAfter; 38 } 39 40 public void setMaxSession(int maxSession) { 41 this.maxSession = maxSession; 42 } 43 44 public void setSessionManager(SessionManager sessionManager) { 45 this.sessionManager = sessionManager; 46 } 47 48 public void setCacheManager(CacheManager cacheManager) { 49 this.cache = cacheManager.getCache("shiro-kickout-session"); 50 } 51 52 @Override 53 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { 54 return false; 55 } 56 57 @Override 58 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 59 Subject subject = getSubject(request, response); 60 if(!subject.isAuthenticated() && !subject.isRemembered()) { 61 //如果沒有登錄,直接進行之后的流程 62 return true; 63 } 64 65 Session session = subject.getSession(); 66 String username = (String) subject.getPrincipal(); 67 Serializable sessionId = session.getId(); 68 69 //TODO 同步控制 70 Deque<Serializable> deque = cache.get(username); 71 if(deque == null) { 72 deque = new LinkedList<Serializable>(); 73 cache.put(username, deque); 74 } 75 76 //如果隊列里沒有此sessionId,且用戶沒有被踢出;放入隊列 77 if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) { 78 deque.push(sessionId); 79 } 80 81 //如果隊列里的sessionId數超出最大會話數,開始踢人 82 while(deque.size() > maxSession) { 83 Serializable kickoutSessionId = null; 84 if(kickoutAfter) { //如果踢出后者 85 kickoutSessionId = deque.removeFirst(); 86 } else { //否則踢出前者 87 kickoutSessionId = deque.removeLast(); 88 } 89 try { 90 Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); 91 if(kickoutSession != null) { 92 //設置會話的kickout屬性表示踢出了 93 kickoutSession.setAttribute("kickout", true); 94 } 95 } catch (Exception e) {//ignore exception 96 } 97 } 98 99 //如果被踢出了,直接退出,重定向到踢出后的地址 100 if (session.getAttribute("kickout") != null) { 101 //會話被踢出了 102 try { 103 subject.logout(); 104 } catch (Exception e) { //ignore 105 } 106 saveRequest(request); 107 WebUtils.issueRedirect(request, response, kickoutUrl); 108 return false; 109 } 110 111 return true; 112 } 113 }
3.測試
因為此處設置maxSession=2,所以需要打開3個瀏覽器。分別訪問:http:l//ocalhost:8080/chapter18 進行登錄。
然后刷新第一次打開的瀏覽器,將會被強制退出。
4.遇到的問題
(1)there is no session Id ***
報錯:there is no session Id ***。
原因:我沒有在ehcache.xml里配置"shiro-kickout-session"。
因為kickoutController里用到了:
1 public void setCacheManager(CacheManager cacheManager) { 2 this.cache = cacheManager.getCache("shiro-kickout-session"); 3 }
所以在ehcache.xml中一定記得加上(名字匹配即可):
1 <cache name="shiro-kickout-session" 2 eternal="false" 3 timeToIdleSeconds="3600" 4 timeToLiveSeconds="0" 5 overflowToDisk="false" 6 statistics="true"> 7 </cache>
(2)sessionKey must be an HTTP compatible implementation
報錯:sessionKey must be an HTTP compatible implementation。
原因:我的sessionManager和示例代碼中的sessionManager不同,示例中用的是DefaultWebSessionManager,我用的是ServletContainerSessionManager。
代碼中這一句報的錯誤:
1 Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
sessionManager.getSession時,因為sessionManager的類類型是ServletContainerSessionManager,所以會進行一個http判定。
參考來自:http://blog.csdn.net/qq_26946497/article/details/51064654?locationNum=3
1 public Session getSession(SessionKey key) throws SessionException { 3 if (!WebUtils.isHttp(key)) { //判斷是不是http的key,否則拋異常 4 String msg = "SessionKey must be an HTTP compatible implementation."; 5 throw new IllegalArgumentException(msg); 6 } 7 ...
14 }
最后的解決辦法:不存放sessionId在deque中,直接存放Session。又可以跳過通過sessionId獲取session這一步,直接從deque中拿到之前保存的session。
1 //修改前 2 Deque<Serializable> deque = cache.get(username); 3 deque.push(sessionId); 4 kickoutSessionId = deque.removeLast(); 5 Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); 6 7 //修改后 8 Deque<Session> deque = cache.get(username); 9 deque.push(session); 10 kickoutSession = deque.removeLast();
(3)沒有增加鎖
1 synchronized (this.cache) { 2 Deque<Session> deque = cache.get(usernameTenant); 3 ... 4 } 5 //如果被踢出了,直接退出,重定向到踢出后的地址 6 if (session.getAttribute(KICK_OUT) != null && session.getAttribute(KICK_OUT) == true) { 7 ...
(4)動態設定是否需要kickout
在配置文件中,設置參數 kickout = false。然后在kickoutController里拿到這個參數的值。
1 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 2 if("false".equals(kickout)){ 3 //如果不需要單用戶登錄的限制 4 return true; 5 } 6 ... 7 }
5.CacheManager和SessionManager詳解
(1)CacheManager
示例中的配置文件:
-> ehcache.xml
-> ehcacheManager(EhCacheManagerFactoryBean)
-> springCacheManager(EhCacheCacheManager)
-> cacheManager(SpringCacheManagerWrapper)
-> 其他bean里使用
我的配置文件:
-> ehcache.xml
-> ehcacheManager(EhCacheManagerFactoryBean)
-> cacheManager(EhCacheCacheManager)
-> springCacheManager(SpringCacheManagerWrapper)
-> 其他bean里使用
所以名字都是浮雲,重點是從cacheManager的構成:
-> ehcache.xml
-> EhCacheManagerFactoryBean
-> EhCacheCacheManager
-> SpringCacheManagerWrapper
-> 其他bean使用
詳細配置如下:
1 spring-config-shiro.xml 2 <bean id="cacheManager" class="com.github.zhangkaitao.shiro.spring.SpringCacheManagerWrapper"> 3 <property name="cacheManager" ref="springCacheManager"/> 4 </bean> 5 6 <bean id="credentialsMatcher" class="com.github.zhangkaitao.shiro.chapter18.credentials.RetryLimitHashedCredentialsMatcher"> 7 <constructor-arg ref="cacheManager"/> 8 ... 9 </bean> 10 11 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> 12 <property name="sessionManager" ref="sessionManager"/> 13 <property name="cacheManager" ref="cacheManager"/> 14 ... 15 </bean> 16 17 <bean id="kickoutSessionControlFilter" class="com.github.zhangkaitao.shiro.chapter18.web.shiro.filter.KickoutSessionControlFilter"> 18 <property name="cacheManager" ref="cacheManager"/> 19 <property name="sessionManager" ref="sessionManager"/> 20 ... 21 </bean> 22 23 spring-config-cache.xml 24 <bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> 25 <property name="cacheManager" ref="ehcacheManager"/> 26 </bean> 27 28 <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> 29 <property name="configLocation" value="classpath:ehcache/ehcache.xml"/> 30 </bean>
(2)SessionManager
SessionManager是一個接口。
1 public interface SessionManager { 2 Session start(SessionContext sessionContext); 3 Session getSession(SessionKey sessionKey) throws SessionException; 5 }
類結構圖如下:
Shiro提供了三個默認實現:
DefaultSessionManager:DefaultSecurityManager使用的默認實現,用於JavaSE環境;
ServletContainerSessionManager:DefaultWebSecurityManager使用的默認實現,用於Web環境,其直接使用Servlet容器的會話;
DefaultWebSessionManager:用於Web環境的實現,可以替代ServletContainerSessionManager,自己維護着會話,直接廢棄了Servlet容器的會話管理。
示例中:配置文件spring-config-shiro.xml中使用的是DefaultWebSessionManager。
1<!-- 會話管理器 --> 3 <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> 4 ... 5 </bean> 6 7 <!-- 安全管理器 --> 8 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> 9 <property name="sessionManager" ref="sessionManager"/> 10 <property name="cacheManager" ref="cacheManager"/> 11 ... 12 </bean> 13
13 <!-- 並發登錄控制 --> 14 <bean id="kickoutSessionControlFilter" class="com.github.zhangkaitao.shiro.chapter18.web.shiro.filter.KickoutSessionControlFilter"> 15 <property name="cacheManager" ref="cacheManager"/> 16 <property name="sessionManager" ref="sessionManager"/> 17 ... 18 </bean>
我的項目中:配置文件applicationContext-shiro.xml中沒有進行sessionManager的配置(為了共享session),所以使用的是shiro的默認實現:ServletContainerSessionManager。(或者運行代碼時,可以去看sessionManager的類類型)
1 <!--文件中沒有sessionManager的配置--> 2 3 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> 4 <property name="cacheManager" ref="springCacheManager"/> 5 <!--這里沒有配置sessionManager--> 6 ... 7 </bean> 8 9 <bean id="kickoutSessionControlFilter" class="com.baosight.common.filter.KickoutSessionControlFilter"> 10 <property name="cacheManager" ref="springCacheManager"/> 11 <!--這里沒有配置sessionManager--> 12 ... 13 </bean>
而這兩種實現(DefaultWebSessionManager 和 ServletContainerSessionManager)的區別以及源碼分析:
http://blog.csdn.net/qq_26946497/article/details/51064654?locationNum=3
注意:沒有配置SessionManager時,默認為ServletContainerSessionManager。