为了安全起见,同一个账号理应同时只能在一台设备上登录,后面登录的踢出前面登录的。用Shiro可以轻松实现此功能。
shiro中sessionManager是专门作会话管理的,而sessinManager将会话保存在sessionDAO中,如果不给sessionManager注入
sessionDAO,会话将是瞬时状态,没有被保存起来,从sessionManager里取session,是取不到的。
此例中sessionDAO注入了Ehcache缓存,会话被保存在Ehcache中,不知Ehcache为何物的,请自行查阅资料。
完整demo下载:http://download.csdn.net/detail/qq_33556185/9555720
shiro.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd"> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="deleteInvalidSessions" value="true"/> <property name="sessionDAO" ref="sessionDAO"/> <property name="sessionIdCookieEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="cacheManager" ref="shiroEhcacheManager"/> </bean> <!-- 会话DAO,sessionManager里面的session需要保存在会话Dao里,没有会话Dao,session是瞬时的,没法从 sessionManager里面拿到session --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="sessionIdGenerator" ref="sessionIdGenerator"/> <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/> </bean> <!-- 缓存管理器 --> <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml" /> </bean> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="-1"/> </bean> <!-- 会话ID生成器 --> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean> <bean id="kickoutSessionControlFilter" class="com.core.shiro.KickoutSessionControlFilter"> <property name="sessionManager" ref="sessionManager"/> <property name="cacheManager" ref="shiroEhcacheManager"/> <property name="kickoutAfter" value="false"/> <property name="maxSession" value="1"/> <property name="kickoutUrl" value="/toLogin?kickout=1"/> </bean> <bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter"> <property name="redirectUrl" value="/toLogin" /> </bean> <!-- 会话验证调度器 --> <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler "> <property name="interval" value="1800000"/> <property name="sessionManager" ref="sessionManager"/> </bean> <!-- Shiro Filter 拦截器相关配置 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- securityManager --> <property name="securityManager" ref="securityManager" /> <!-- 登录路径 --> <property name="loginUrl" value="/toLogin" /> <!-- 用户访问无权限的链接时跳转此页面 --> <property name="unauthorizedUrl" value="/unauthorizedUrl.jsp" /> <!-- 过滤链定义 --> <property name="filters"> <map> <entry key="kickout" value-ref="kickoutSessionControlFilter"/> </map> </property> <property name="filterChainDefinitions"> <value> /loginin=kickout,anon /logout = logout /toLogin=anon /css/**=anon /html/**=anon /images/**=anon /js/**=anon /upload/**=anon <!-- /userList=roles[admin] --> /userList=kickout,authc,perms[/userList] /toRegister=kickout,authc,perms[/toRegister] /toDeleteUser=kickout,authc,perms[/toDeleteUser] /** = kickout,authc </value> </property> </bean> <!-- securityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm" /> <property name="sessionManager" ref="sessionManager"/> <property name="cacheManager" ref="shiroEhcacheManager"/> </bean> <!-- 自定义Realm实现 --> <bean id="myRealm" class="com.core.shiro.CustomRealm" /> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"></property> </bean> </beans>ehcache.xml
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false" maxBytesLocalDisk="20G" maxBytesLocalOffHeap="50M"> <diskStore path="java.io.tmpdir"/> <!-- 系统的默认临时文件路径 --> <defaultCache eternal="false" maxElementsInMemory="10000" overflowToDisk="false" timeToIdleSeconds="0" timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" /> <!-- eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。 maxElementsInMemory:缓存中允许创建的最大对象数 overflowToDisk:内存不足时,是否启用磁盘缓存。 timeToIdleSeconds:缓存数据的钝化时间,也就是在一个元素消亡之前, 两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效, 如果该值是 0 就意味着元素可以停顿无穷长的时间。 timeToLiveSeconds:缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值, 这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。 memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。 1 FIFO,先进先出 2 LFU,最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。 3 LRU,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。 --> <cache name="myCache" eternal="false" maxElementsInMemory="10000" overflowToDisk="false" timeToIdleSeconds="0" timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" /> <cache name="shiro-activeSessionCache" eternal="false" maxElementsInMemory="10000" overflowToDisk="true" timeToIdleSeconds="0" timeToLiveSeconds="0"/> </ehcache>login.jsp的j avascript:
<script type="text/javascript"> function kickout(){ var href=location.href; if(href.indexOf("kickout")>0){ alert("您的账号在另一台设备上登录,您被挤下线,若不是您本人操作,请立即修改密码!"); } } window.onload=kickout(); </script>KickoutSessionControlFilter:
package com.core.shiro; import java.io.Serializable; import java.util.Deque; import java.util.LinkedList; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; public class KickoutSessionControlFilter extends AccessControlFilter{ private String kickoutUrl; //踢出后到的地址 private boolean kickoutAfter; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 private int maxSession; //同一个帐号最大会话数 默认1 private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } public void setCacheManager(CacheManager cacheManager) { this.cache = cacheManager.getCache("shiro-activeSessionCache"); } /** * 是否允许访问,返回true表示允许 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } /** * 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。 */ @Override 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(); // 初始化用户的队列放到缓存里 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.getFirst(); 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 e.printStackTrace(); } } //如果被踢出了,直接退出,重定向到踢出后的地址 if (session.getAttribute("kickout") != null) { //会话被踢出了 try { subject.logout(); } catch (Exception e) { } WebUtils.issueRedirect(request, response, kickoutUrl); return false; } return true; } }