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 }
fighting for this