shiro自定义拦截器继承AccessControllerFilter,实现session互踢机制。
应用场景:
我们经常会有用到,当A 用户在北京登录 ,然后A用户在天津再登录 ,要踢出北京登录的状态。如果用户在北京重新登录,那么又要踢出天津的用户,这样反复。又或是需要限制同一用户的同时在线数量,超出限制后,踢出最先登录的或是踢出最后登录的。
分析:
spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。那就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这2个方法:isAccessAllowed、onAccessDenied
isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
部分代码:
public class KickOutSessionControlFilter extends AccessControlFilter {
private static final Logger logger = LoggerFactory.getLogger(KickOutSessionControlFilter.class);
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickOutAfter = false;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession = 1;
/**
* 会话管理器
*/
private SessionManager sessionManager;
/**
* 会话缓存
*/
private Cache<String, Deque<Serializable>> cache;
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(RedisCacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro_redis_cache");
}
/**
* 是否允许访问,返回true表示允许
*
* @param servletRequest
* @param servletResponse
* @param obj
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object obj) {
return false;
}
/**
* 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
*
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 1同一个用户在不同ip上,不可以同时访问,后者会把前者踢出,即同一个用户不可以同时访问
Subject subject = getSubject(servletRequest, servletResponse);
System.out.println("===当前subject:==" + SecurityUtils.getSubject());
if (!subject.isAuthenticated() && !subject.isRemembered()) {
// 如果没有登录,直接进行之后的拦截器链
return true;
}
// 当前用户
User user = (User) subject.getPrincipal();
String username = user.getUserName();
// 当前会话
Session session = subject.getSession();
Serializable sessionId = session.getId();
// 读取缓存用户 没有就存入
Deque<Serializable> deque = cache.get(username);
if (deque == null) {
// 初始化队列
deque = new ArrayDeque<Serializable>();
}
// 如果队列里没有当前会话sessionId,且当前会话未设置踢出标记(用户没有被踢出),放入队列
if (!deque.contains(sessionId) && session.getAttribute("kickOut") == null) {
// 将用户的sessionId存入队列
deque.push(sessionId);
// 将用户的sessionId存入队列缓存
cache.put(username, deque);
}
// 如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
Serializable kickOutSessionId = null;
// 是否踢出后来登录的,默认是false,即后者登录的用户踢出前者登录的用户;
if (kickOutAfter) {
// 如果踢出后者
kickOutSessionId = deque.removeFirst();
} else {
// 否则踢出前者
kickOutSessionId = deque.removeLast();
}
// 踢出后再更新下缓存队列
cache.put(username, deque);
try {
// 获取被踢出的sessionId的session对象
Session kickOutSession = sessionManager.getSession(new DefaultSessionKey(kickOutSessionId));
if (kickOutSession != null) {
// 设置会话的kickOut属性表示踢出了
kickOutSession.setAttribute("kickOut", true);
System.out.println("===将sessionId:==" + kickOutSession.getId() + "设置踢出标记");
}
} catch (Exception e) {
// ignore exception
}
}
// ajax请求,如果被踢出了,(前者或后者)直接退出,返回相应的状态
if (session.getAttribute("kickOut") != null && (Boolean) session.getAttribute("kickOut") == true) {
// 当前会话踢出标记不为空且等于true,会话被踢出了
try {
// 退出登录
String ip = IPUtil.getIpAddress((HttpServletRequest) servletRequest);
String url = ((HttpServletRequest) servletRequest).getRequestURL() + "";
SecurityLogoutFilter.logout(subject);
logger.info("IP地址为:" + ip + "的用户【" + username + "】被踢出,已在其他ip地址登录");
ResponseUtil.returnResultAjax();
return false;
} catch (Exception e) {
// ignore
}
return false;
}
return true;
}
}
shiroFilterFactoryBean方法
// 自定义过滤器
Map<String, Filter> filters = new HashMap<>();
// 同一用户登陆互踢
filters.put("kickOut", kickOutSessionControlFilter());
filterChainDefinitionMap.put("/**", "kickOut,authc");
定义拦截器的时候不需要加@Bean;
//@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter(){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//用于根据会话ID,获取会话进行踢出操作的;
//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
kickoutSessionControlFilter.setKickoutAfter(false);
//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
kickoutSessionControlFilter.setMaxSession(1);
//被踢出后重定向到的地址
kickoutSessionControlFilter.setKickoutUrl("/a/login");
return kickoutSessionControlFilter;
}
互踢分析:
1A用户第一次访问登录,进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器均未拦截住,进行正常登录获得会话token
2.1A用户第一次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量未超过1,当前会话的标记为空进入后面的拦截器
2.11A用户第二次访问请求,deque未变化,当前会话的标记为空进入后面的拦截器
2.2B用户第一次访问登录,进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器未拦截住,进行正常登录获得会话token
2.21B用户第一次访问请求,进入互踢过滤器,获取当前会话,从cache中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量超过1,将deque中A的token删除,cache更新,将A的会话设置标记,当前会话的标记为空进入后面的拦截器
2.22B用户第二次访问请求,deque未变化,当前会话的标记为空进入后面的拦截器
3.1A用户第三次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token但当前会话已设置标记不更新deque和cache,deque中的token数量未超过1,当前会话的标记不为空且为true,进行登出返回
3.2A用户第四次访问请求,会话过期请重新登录
4.1A用户第二次访问登录,进入进入互踢过滤器,获取当前会话,会话未认证不处理进入后面的拦截器,拦截器均未拦截住,进行正常登录获得会话token
4.2A用户第四次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token且当前会话未设置标记,将会话token放入deque,放入cache,deque中token数量超过1,将deque中B的token删除,cache更新,将B的会话设备标记,当前会话的标记为空进入后面的拦截器
5B用户第三次访问请求,进入互踢过滤器,获取当前会话,从cahe中获取deque,该deque不包含当前会话token但当前会话已设置标记不更新deque和cache,deque中的token数量未超过1,当前会话的标记不为空且为true,进行登出返回