2017.4.12 開濤shiro教程-第十八章-並發登錄人數控制


原博客地址: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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM