2017.6.30 用shiro实现并发登录人数控制(实际项目中的实现)

之前的学习总结: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 }
 
 
 
原文地址:https://www.cnblogs.com/lyh421/p/7099541.html