并发登录配置
<!-- 会话管理器 -->
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="1800000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionDAO" ref="sessionDAO" />
<property name="sessionIdCookieEnabled" value="true" />
<property name="sessionIdCookie" ref="sessionIdCookie" />
<property name="cacheManager" ref="cacheManager" />
</bean>
<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="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
<!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
<property name="interval" value="1800000" />
<property name="sessionManager" ref="sessionManager" />
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="sessionManager" ref="sessionManager"/>
....
</bean>
<!-- 自定义filter -->
<bean id="KickoutSessionControlFilter" class="com.zm.web_shiro.web.filter.KickoutSessionControlFilter" >
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="kickoutAfter" value="false"/>
<property name="maxSession" value="1"/>
<property name="kickoutUrl" value="/jsp/login.jsp?kickout=1"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/jsp/login.jsp"/>
<property name="unauthorizedUrl" value="/jsp/unauthorized.jsp"/>
<property name="filterChainDefinitions">
<value>
/index.jsp = anon
/unauthorized.jsp = anon
/jsp/login.jsp = anon
/jsp/admin.jsp = authc,KickoutSessionControlFilter
/jsp/user.jsp = user,KickoutSessionControlFilter
/login/doLogout = logout
/login/code = VcodeControlFilter
/login/** = accessControlFilter
/jsp/** = user,KickoutSessionControlFilter
<!-- 资源 -->
/css/** = anon
/js/** = anon
</value>
</property>
<property name="filters">
<map>
<entry key="accessControlFilter" value-ref="accessControlFilter" />
<entry key="KickoutSessionControlFilter" value-ref="KickoutSessionControlFilter" />
<entry key="VcodeControlFilter" value-ref="VcodeControlFilter" />
</map>
</property>
</bean>
接下来就是把配置中的KickoutSessionControlFilter实现了
public class KickoutSessionControlFilter extends AccessControlFilter {
private static final Logger log = LogManager.getLogger(KickoutSessionControlFilter.class);
private String kickoutUrl; //踢出后到的地址
private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
private int maxSession = 1; //同一个帐号最大会话数 默认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-kickout-session");
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return 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();
log.info("username:" + username );
log.info("sessionId:" + sessionId );
//TODO 同步控制
Deque<Serializable> deque = cache.get(username);
log.info("deque:" + deque);
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();
log.info("kickoutAfter:" + kickoutAfter + ",踢出后者");
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
log.info("kickoutAfter:" + kickoutAfter + ",踢出前者");
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
log.info("设置会话的kickout属性表示踢出了");
}
} catch (Exception e) {//ignore exception
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
log.info("会话被踢出了");
//会话被踢出了
try {
subject.logout();
log.info("subject.logout(),登出并清理缓存");
} catch (Exception e) { //ignore
}
saveRequest(request);
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
}
运行效果如下(挤人需要用到两个浏览器):