public class KickoutSessionControlFilter extends AccessControlFilter {
private static final Logger log = LoggerFactory.getLogger(KickoutSessionControlFilter.class);
/**
* 踢出后到的地址
**/
private String kickoutUrl;
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
**/
private Boolean kickoutAfter = false;
/**
* 同一个账号最大会话数 默认5
**/
private int maxSession = 5;
private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;
/**
* * 表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
* * (感觉这里应该是对白名单(不需要登录的接口)放行的)
* * 如果isAccessAllowed返回true则onAccessDenied方法不会继续执行 因为底层是 (isAccessAllowed || onAccessDenied)
* * 这里可以用来判断一些不被通过的链接(个人备注)
* * * 表示是否允许访问 ,如果允许访问返回true,否则false;
* ---------------------
* 版权声明:本文为CSDN博主「月未明」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
* 原文链接:https://blog.csdn.net/qq_35981283/article/details/78633692
* @param request
* @param response
* @param mappedValue
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
/**
* 表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
* onAccessDenied是否执行取决于isAccessAllowed的值,如果返回true则onAccessDenied不会执行;如果返回false,执行onAccessDenied
* 如果onAccessDenied也返回false,则直接返回,不会进入请求的方法(只有isAccessAllowed和onAccessDenied的情况下)
* */
@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();
SysUser sysuser = (SysUser) subject.getPrincipal();
String username = sysuser.getUsername();
Serializable sessionId = session.getId();
//读取缓存 没有就存入
Deque<Serializable> deque = cache.get(username);
//如果此用户没有session队列,说明没有登录过,缓存中没有,new一个空队列
if (null == deque) {
deque = new LinkedList<Serializable>();
}
//如果队列里没有sessionId,且用户没有被踢出,加入队列
if (!deque.contains(sessionId) && null == session.getAttribute("kickout")) {
//将sessionId加入队列
deque.push(sessionId);
//将用户sessionId队列缓存
cache.put(username, deque);
}
//如果队列中sessionId数量超过最大会话数,开始踢人
while (deque.size() > maxSession) {
Serializable kickoutSessionId = null;
//踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
if (kickoutAfter) {
kickoutSessionId = deque.removeFirst();
//踢出后更新缓存队列
cache.put(username, deque);
} else {
kickoutSessionId = deque.removeLast();
cache.put(username, deque);
}
try {
//获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (null != kickoutSession) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {
//面对异常,我们选择忽略
}
}
//如果被踢出了,直接退出,然后重定向到踢出后的地址
if (null != (Boolean) session.getAttribute("kickout") && (Boolean) session.getAttribute("kickout") == true) {
//会话被踢出了
try {
//退出登录
subject.logout();//会删除对应的session
} catch (Exception e) {
//面对异常,我们选择忽略
}
saveRequest(request);//主要作用是 通过response 返回一个新的放上一个新的cookie(JSESSIONID),此处的cookie还没变,下次请求才带新的,比如下面的重定向
Map<String, String> resultMap = new HashMap<String, String>(2);
//判断是不是Ajax请求
if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
resultMap.put("user_status", "300");
resultMap.put("message", "强制退出,您已经在其他地方登录,请重新登录!");
//输出json串
out(response, resultMap);
} else {
//重定向 会带着新的cookie
WebUtils.issueRedirect(request, response, kickoutUrl);
}
}
return true;//true放行
}
private void out(ServletResponse response, Map<String, String> map) throws IOException {
try {
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.println();
out.close();
} catch (Exception e) {
//面对异常,我们选择忽略
log.info("[KickoutSessionFilter.class 输出JSON异常,可以忽略。]-[{}]", new Date());
}
}
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_redis_cache");
}
}
上面的自定义的过滤器要在config里放到过滤链中(anon user perm 其实都是些内置的过滤器)
要让该过滤器起作用还要在filterChainDefinitionMap put一下,本过滤器是作最大在线人数踢出用的所以放在最前面(规则为(/ ** / ** ) 。自测 / ** 不好用,不知为啥?还请大家指导),当然为了要让该过滤器后面的也正常执行,该过滤器最后要返回true(文开头的那段代码标红的字体),否则后面的/css/** anon等过滤规则就失效了