`
jinnianshilongnian
  • 浏览: 21430640 次
  • 性别: Icon_minigender_1
博客专栏
5c8dac6a-21dc-3466-8abb-057664ab39c7
跟我学spring3
浏览量:2404348
D659df3e-4ad7-3b12-8b9a-1e94abd75ac3
Spring杂谈
浏览量:2997101
43989fe4-8b6b-3109-aaec-379d27dd4090
跟开涛学SpringMVC...
浏览量:5631001
1df97887-a9e1-3328-b6da-091f51f886a1
Servlet3.1规范翻...
浏览量:257411
4f347843-a078-36c1-977f-797c7fc123fc
springmvc杂谈
浏览量:1592961
22722232-95c1-34f2-b8e1-d059493d3d98
hibernate杂谈
浏览量:248900
45b32b6f-7468-3077-be40-00a5853c9a48
跟我学Shiro
浏览量:5846927
Group-logo
跟我学Nginx+Lua开...
浏览量:697914
5041f67a-12b2-30ba-814d-b55f466529d5
亿级流量网站架构核心技术
浏览量:780235
社区版块
存档分类
最新评论

第十八章 并发登录人数控制——《跟我学Shiro》

阅读更多

 

目录贴: 跟我学Shiro目录贴

 

在某些项目中可能会遇到如每个账户同时只能有一个人登录或几个人同时登录,如果同时有多人登录:要么不让后者登录;要么踢出前者登录(强制退出)。比如spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。

 

示例代码基于《第十六章 综合实例》完成,通过Shiro Filter机制扩展KickoutSessionControlFilter完成。

 

首先来看看如何配置使用(spring-config-shiro.xml

  

kickoutSessionControlFilter用于控制并发登录人数的 

<bean id="kickoutSessionControlFilter" 
class="com.github.zhangkaitao.shiro.chapter18.web.shiro.filter.KickoutSessionControlFilter">
    <property name="cacheManager" ref="cacheManager"/>
    <property name="sessionManager" ref="sessionManager"/>

    <property name="kickoutAfter" value="false"/>
    <property name="maxSession" value="2"/>
    <property name="kickoutUrl" value="/login?kickout=1"/>
</bean> 

cacheManager:使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;

sessionManager:用于根据会话ID,获取会话进行踢出操作的;

kickoutAfter:是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;

maxSession:同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;

kickoutUrl:被踢出后重定向到的地址;

 

shiroFilter配置 

   <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="filters">
            <util:map>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
                <entry key="sysUser" value-ref="sysUserFilter"/>
                <entry key="kickout" value-ref="kickoutSessionControlFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login = authc
                /logout = logout
                /authenticated = authc
                /** = kickout,user,sysUser
            </value>
        </property>
    </bean> 

此处配置除了登录等之外的地址都走kickout拦截器进行并发登录控制。

 

测试

此处因为maxSession=2,所以需要打开3个浏览器(需要不同的浏览器,如IEChromeFirefox),分别访问http://localhost:8080/chapter18/进行登录;然后刷新第一次打开的浏览器,将会被强制退出,如显示下图: 


KickoutSessionControlFilter核心代码: 

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    Subject subject = getSubject(request, response);
    if(!subject.isAuthenticated() && !subject.isRemembered()) {
        //如果没有登录,直接进行之后的流程
        return true;
    }

    Session session = subject.getSession();
    String username = (String) subject.getPrincipal();
    Serializable sessionId = session.getId();

    //TODO 同步控制
    Deque<Serializable> deque = cache.get(username);
    if(deque == null) {
        deque = new LinkedList<Serializable>();
        cache.put(username, deque);
    }

    //如果队列里没有此sessionId,且用户没有被踢出;放入队列
    if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
        deque.push(sessionId);
    }

    //如果队列里的sessionId数超出最大会话数,开始踢人
    while(deque.size() > maxSession) {
        Serializable kickoutSessionId = null;
        if(kickoutAfter) { //如果踢出后者
            kickoutSessionId = deque.removeFirst();
        } else { //否则踢出前者
            kickoutSessionId = deque.removeLast();
        }
        try {
            Session kickoutSession =
                sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
            if(kickoutSession != null) {
                //设置会话的kickout属性表示踢出了
                kickoutSession.setAttribute("kickout", true);
            }
        } catch (Exception e) {//ignore exception
        }
    }

    //如果被踢出了,直接退出,重定向到踢出后的地址
    if (session.getAttribute("kickout") != null) {
        //会话被踢出了
        try {
            subject.logout();
        } catch (Exception e) { //ignore
        }
        saveRequest(request);
        WebUtils.issueRedirect(request, response, kickoutUrl);
        return false;
    }
    return true;
} 

此处使用了Cache缓存用户名—会话id之间的关系;如果量比较大可以考虑如持久化到数据库/其他带持久化的Cache中;另外此处没有并发控制的同步实现,可以考虑根据用户名获取锁来控制,减少锁的粒度。

 

另外可参考JavaEE项目开发脚手架,其提供了后台踢出用户的功能:

https://github.com/zhangkaitao/es/blob/master/web/src/main/java/com/sishuok/es/sys/user/web/controller/UserOnlineController.java 

    

 

 

示例源代码:https://github.com/zhangkaitao/shiro-example;可加群 231889722 探讨Spring/Shiro技术。

        

  

13
1
分享到:
评论
24 楼 lvyuan1234 2018-03-06  
zhaoml529 写道
踢出后,前台显示“您被踢出登录” 这个是从哪设置,subject.logout(); 退出放入request, 前台获取不到。

在login.jsp里面,有${not empty param.kickout}相当于<%=request.getParameter("kickout")%>  ,而filter中设置过kickoutSession.setAttribute("kickout", true);
23 楼 sbwfgihc 2016-08-12  
哭泣的yu 写道
zhaoml529 写道
踢出后,前台显示“您被踢出登录” 这个是从哪设置,subject.logout(); 退出放入request, 前台获取不到。



怎么获取的
22 楼 哭泣的yu 2016-07-14  
zhaoml529 写道
踢出后,前台显示“您被踢出登录” 这个是从哪设置,subject.logout(); 退出放入request, 前台获取不到。

21 楼 lwadsy 2016-07-01  
11111
20 楼 pyzheng 2016-05-09  
z5293064 写道
为什么不在其他人登录时直接踢出呢



我个人认为, 这个例子只是例子而已, 在生产环境的时候, 没必要这么做,  在登录的入口做这个功能就好  避免性能上没必要的开销
19 楼 z5293064 2016-02-03  
为什么不在其他人登录时直接踢出呢
18 楼 pyzheng 2015-12-15  
newboy2004 写道
pyzheng 写道
qq787681335 写道
看到楼上一朋友说道会报一个there is no session id XXXXX错误,我也遇到了,是通过同一个浏览器打开多个窗口登陆同一用户时候出现的,如果是不同的浏览器登陆同一个用户是没有问题的。
出错代码行  Session kickoutSession =  sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));

请问一下 同一个浏览器下多个窗口(非标签页)登陆同一用户是怎么处理的。这种情况是没法踢出先登录用户的。感谢!


同一个浏览器同一样的session吧


同一个浏览器的不同tab是同一个session的



是的  可恶的12306以前可以多个浏览器同时登录  现在不行了,  但是可以同一个浏览器多个标签登录.
17 楼 newboy2004 2015-12-15  
pyzheng 写道
qq787681335 写道
看到楼上一朋友说道会报一个there is no session id XXXXX错误,我也遇到了,是通过同一个浏览器打开多个窗口登陆同一用户时候出现的,如果是不同的浏览器登陆同一个用户是没有问题的。
出错代码行  Session kickoutSession =  sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));

请问一下 同一个浏览器下多个窗口(非标签页)登陆同一用户是怎么处理的。这种情况是没法踢出先登录用户的。感谢!


同一个浏览器同一样的session吧


同一个浏览器的不同tab是同一个session的
16 楼 pyzheng 2015-12-01  
qq787681335 写道
看到楼上一朋友说道会报一个there is no session id XXXXX错误,我也遇到了,是通过同一个浏览器打开多个窗口登陆同一用户时候出现的,如果是不同的浏览器登陆同一个用户是没有问题的。
出错代码行  Session kickoutSession =  sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));

请问一下 同一个浏览器下多个窗口(非标签页)登陆同一用户是怎么处理的。这种情况是没法踢出先登录用户的。感谢!


同一个浏览器同一样的session吧
15 楼 626872972 2015-11-05  
涛ge,我配置了SpringCacheManagerWrapper后 登录后进入KickoutSessionControlFilter检查  获取cache.get(username)  报空指针 cache里没有登录信息的缓存  是为什么呢?  我的配置都是按照你的配的    
14 楼 pyzheng 2015-10-26  
其实楼主, 你这个是并发控制人数    我才开始想了解这个  但我更想之后的不是并发人数  而是在线人数的控制   请问它支持在线人数控制么?
谢谢
13 楼 pyzheng 2015-10-26  
qq787681335 写道
看到楼上一朋友说道会报一个there is no session id XXXXX错误,我也遇到了,是通过同一个浏览器打开多个窗口登陆同一用户时候出现的,如果是不同的浏览器登陆同一个用户是没有问题的。
出错代码行  Session kickoutSession =  sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));

请问一下 同一个浏览器下多个窗口(非标签页)登陆同一用户是怎么处理的。这种情况是没法踢出先登录用户的。感谢!


这个我不清楚  我也想学这个玩意  但无意看到你这个问题  感觉很奇怪  只能一个tab  这个需求是不是太霸道了  难道你要强制用户只能用一个标签不成?  难道你真的真绝对的单页面应用程序?  需求很理想  但有技术的人一定很讨厌被限制在一个标签里面的  好像主流浏览器都当成一个session啊
12 楼 qq787681335 2015-05-19  
看到楼上一朋友说道会报一个there is no session id XXXXX错误,我也遇到了,是通过同一个浏览器打开多个窗口登陆同一用户时候出现的,如果是不同的浏览器登陆同一个用户是没有问题的。
出错代码行  Session kickoutSession =  sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));

请问一下 同一个浏览器下多个窗口(非标签页)登陆同一用户是怎么处理的。这种情况是没法踢出先登录用户的。感谢!
11 楼 zqb666kkk 2015-01-19  
baojieearth 写道
iwindyforest 写道
关于本章我有两个问题:
1. 同步控制
关于同步控制, 我用下面代码实现了, 不知道有没有更有效率的方法?
synchronized (this.cache) {
			Deque<Serializable> deque = this.cache.get(userName);
			if (deque == null) {
				deque = new ConcurrentLinkedDeque<Serializable>();
			}

			if (!deque.contains(currentSessionId) && session.getAttribute("kickOut") == null) {
				session.setAttribute("userName", userName);
				deque.addLast(currentSessionId);
			}
			this.logger.debug("logged user:" + userName + ", deque size = " + deque.size());
			this.logger.debug("deque = " + deque);
			if (deque.size() > this.maxSession) {
				if (!this.kickOutTail) {
					kickOutSessionId = deque.removeFirst();
					this.logger.debug("kick out head session: " + kickOutSessionId);
				} else {
					kickOutSessionId = deque.removeLast();
					this.logger.debug("kick out tail session: " + kickOutSessionId);
				}
			}

			this.cache.put(userName, deque);
		}


2.deque存sessionId的bug
其实存在deque里面的session是有可能变无效的, 比如过期或者用户退出登录, 所以sessionId也需要根据session的状态来维护, 想来想去没想到更好的方法, 写了个SessionListener来做的, 实现是以下:
public class KickOutSessionListener implements SessionListener {
	private Logger logger = LoggerFactory.getLogger(this.getClass().getName());
	private Cache<String, Deque<Serializable>> cache;

	public void setCacheManager(CacheManager cacheManager) {
		this.cache = cacheManager.getCache("kickOutSessionCache");
	}

	private void removeSessionFromCache(Session session) {
		String userName = (String) session.getAttribute("userName");
		if (userName != null) {
			this.logger.debug("remove session: " + session.getId() + " in deque of " + userName
					+ "@kickOutSessionCache");
			synchronized (this.cache) {
				Deque<Serializable> deque = this.cache.get(userName);
				deque.remove(session.getId());
				this.cache.put(userName, deque);
			}
		}
	}	

	@Override
	public void onStop(Session session) {
		this.removeSessionFromCache(session);
	}

	@Override
	public void onExpiration(Session session) {
		this.removeSessionFromCache(session);
	}

}
我按你这个做了,功能上没问题  就是有时候会报there is no session id XXXXX的错误 或者这个 org.apache.shiro.session.StoppedSessionException ,不知道为什么

为什么我没遇到
10 楼 baojieearth 2015-01-16  
iwindyforest 写道
关于本章我有两个问题:
1. 同步控制
关于同步控制, 我用下面代码实现了, 不知道有没有更有效率的方法?
synchronized (this.cache) {
			Deque<Serializable> deque = this.cache.get(userName);
			if (deque == null) {
				deque = new ConcurrentLinkedDeque<Serializable>();
			}

			if (!deque.contains(currentSessionId) && session.getAttribute("kickOut") == null) {
				session.setAttribute("userName", userName);
				deque.addLast(currentSessionId);
			}
			this.logger.debug("logged user:" + userName + ", deque size = " + deque.size());
			this.logger.debug("deque = " + deque);
			if (deque.size() > this.maxSession) {
				if (!this.kickOutTail) {
					kickOutSessionId = deque.removeFirst();
					this.logger.debug("kick out head session: " + kickOutSessionId);
				} else {
					kickOutSessionId = deque.removeLast();
					this.logger.debug("kick out tail session: " + kickOutSessionId);
				}
			}

			this.cache.put(userName, deque);
		}


2.deque存sessionId的bug
其实存在deque里面的session是有可能变无效的, 比如过期或者用户退出登录, 所以sessionId也需要根据session的状态来维护, 想来想去没想到更好的方法, 写了个SessionListener来做的, 实现是以下:
public class KickOutSessionListener implements SessionListener {
	private Logger logger = LoggerFactory.getLogger(this.getClass().getName());
	private Cache<String, Deque<Serializable>> cache;

	public void setCacheManager(CacheManager cacheManager) {
		this.cache = cacheManager.getCache("kickOutSessionCache");
	}

	private void removeSessionFromCache(Session session) {
		String userName = (String) session.getAttribute("userName");
		if (userName != null) {
			this.logger.debug("remove session: " + session.getId() + " in deque of " + userName
					+ "@kickOutSessionCache");
			synchronized (this.cache) {
				Deque<Serializable> deque = this.cache.get(userName);
				deque.remove(session.getId());
				this.cache.put(userName, deque);
			}
		}
	}	

	@Override
	public void onStop(Session session) {
		this.removeSessionFromCache(session);
	}

	@Override
	public void onExpiration(Session session) {
		this.removeSessionFromCache(session);
	}

}
我按你这个做了,功能上没问题  就是有时候会报there is no session id XXXXX的错误 或者这个 org.apache.shiro.session.StoppedSessionException ,不知道为什么
9 楼 zhaoml529 2015-01-14  
踢出后,前台显示“您被踢出登录” 这个是从哪设置,subject.logout(); 退出放入request, 前台获取不到。
8 楼 560130911 2014-04-29  
请注意及时更新缓存,否则将存在BUG
7 楼 iwindyforest 2014-04-22  
关于本章我有两个问题:
1. 同步控制
关于同步控制, 我用下面代码实现了, 不知道有没有更有效率的方法?
synchronized (this.cache) {
			Deque<Serializable> deque = this.cache.get(userName);
			if (deque == null) {
				deque = new ConcurrentLinkedDeque<Serializable>();
			}

			if (!deque.contains(currentSessionId) && session.getAttribute("kickOut") == null) {
				session.setAttribute("userName", userName);
				deque.addLast(currentSessionId);
			}
			this.logger.debug("logged user:" + userName + ", deque size = " + deque.size());
			this.logger.debug("deque = " + deque);
			if (deque.size() > this.maxSession) {
				if (!this.kickOutTail) {
					kickOutSessionId = deque.removeFirst();
					this.logger.debug("kick out head session: " + kickOutSessionId);
				} else {
					kickOutSessionId = deque.removeLast();
					this.logger.debug("kick out tail session: " + kickOutSessionId);
				}
			}

			this.cache.put(userName, deque);
		}


2.deque存sessionId的bug
其实存在deque里面的session是有可能变无效的, 比如过期或者用户退出登录, 所以sessionId也需要根据session的状态来维护, 想来想去没想到更好的方法, 写了个SessionListener来做的, 实现是以下:
public class KickOutSessionListener implements SessionListener {
	private Logger logger = LoggerFactory.getLogger(this.getClass().getName());
	private Cache<String, Deque<Serializable>> cache;

	public void setCacheManager(CacheManager cacheManager) {
		this.cache = cacheManager.getCache("kickOutSessionCache");
	}

	private void removeSessionFromCache(Session session) {
		String userName = (String) session.getAttribute("userName");
		if (userName != null) {
			this.logger.debug("remove session: " + session.getId() + " in deque of " + userName
					+ "@kickOutSessionCache");
			synchronized (this.cache) {
				Deque<Serializable> deque = this.cache.get(userName);
				deque.remove(session.getId());
				this.cache.put(userName, deque);
			}
		}
	}	

	@Override
	public void onStop(Session session) {
		this.removeSessionFromCache(session);
	}

	@Override
	public void onExpiration(Session session) {
		this.removeSessionFromCache(session);
	}

}
6 楼 zqb666kkk 2014-04-04  
Long_yuan 写道
bug 被强制退出的账号 不一定是前一个 也可能是后一个  - -

没遇到你说的这个问题
5 楼 Long_yuan 2014-04-02  
bug 被强制退出的账号 不一定是前一个 也可能是后一个  - -

相关推荐

Global site tag (gtag.js) - Google Analytics