登陆人数控制,比如同一个用户不能在两个地方登陆。Shiro主要基于自定义的Fliter实现的。本文在上一篇文章 Shiro功能应用(四)–Session管理及在线人数统计代码基础进行添加登陆人数控制。
代码实现:
代码地址:
https://github.com/OooooOz/SpringBoot-Shiro
ShiroConfig的过滤器:
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
... ... ... ...
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("kickout", kickoutSessionControlFilter()); //限制同一帐号同时在线的个数
shiroFilterFactoryBean.setFilters(filtersMap);
//-----------------------------过虑器链定义------------------------------//
LinkedHashMap<String, String> perms = new LinkedHashMap<>();
//其他资源都需要认证 authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址
perms.put("/userList.do", "user,kickout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(perms); //把权限过滤map设置shiroFilterFactoryBean
return shiroFilterFactoryBean;
}
ShiroConfig的自定义人数控制过滤器:
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager());
//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
kickoutSessionControlFilter.setCacheManager(getEhCacheManager());
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/toLogin.do?kickout=1");
return kickoutSessionControlFilter;
}
自定义人数控制过滤器类:
package com.demo.config;
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;
import com.demo.entity.User;
/**
* @description: shiro 自定义filter 实现 并发登录控制
*/
public class KickoutSessionControlFilter extends AccessControlFilter{
/** 踢出后到的地址 */
private String kickoutUrl;
/** 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 */
private boolean kickoutAfter = false;
/** 同一个帐号最大会话数 默认1 */
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 {
System.out.println("--------------------isAccessAllowed");
return false;
}
//表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("--------------------onAccessDenied");
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
String username = ((User) subject.getPrincipal()).getUserName();
Serializable sessionId = session.getId(); //获取sessionId
// 初始化用户的队列放到缓存里
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;
}
}
执行过程:
1).访问http://localhost:8080/userList.do跳转登陆的时候,先执行认证的过滤器,即user对应的过滤器,然后再执行自定义的过滤器kickout(perms.put("/userList.do", “user,kickout”);过滤器链设置的)
2).在kickout的过滤器中:
如果isAccessAllowed返回false,则继续访问onAccessDenied
如果onAccessDenied返回false,则表示在本方法已经处理,就不执行后续的/userList.do控制器处理
如果isAccessAllowed返回true,则不继续访问onAccessDenied,直接执行后续控制器处理(当前过滤器结束)
3).在onAccessDenied方法中:
设置了EHCache缓存(kickoutSessionControlFilter.setCacheManager(getEhCacheManager());)第一个用户登陆,缓存无数据,deque为null,然后把这个用户队列put进缓存,后面把sessionId也push进用户队列deque里,第一个登陆的用户没有超过最大会话数,也不是踢出的会话。所以返回true去执行后续控制器逻辑。
第二个用户登录的时候,也会把sessionId也push进用户队列deque里,但是超过了最大会话数量maxSession,就会将队列的sessionId移除( kickoutSessionId = deque.removeLast();返回移除的sessionId),然后被踢出队列的session设置会话属性(kickoutSession.setAttribute(“kickout”, true);),然后被踢出的session对应用户退出登录。
被踢出用户再次访问时,由于session的会话属性不为null,所以无法将sessionId加入队列中,然后返回false,除非再次登陆,踢出其它登陆的session。
参考文章:
springboot整合shiro-在线人数以及并发登录人数控制(七)