在傳統的項目中,同一賬戶是允許多人同時登錄在線的,有的使用場景恰恰是不允許多人同時在線的,那么我們可以通過 Shiro 來控制并發登錄,并實現后登錄的用戶,擠掉前面登錄的用戶
package com.asurplus.common.shiro;
import com.asurplus.system.entity.SysUserInfo;
import org.apache.shiro.SecurityUtils;
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 javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
/**
* 同一用戶登錄后踢出前面的用戶
*/
public class KickoutSessionFilter extends AccessControlFilter {
/**
* 踢出后到的地址
*/
private String kickoutUrl;
/**
* 踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶
*/
private boolean kickoutAfter = false;
/**
* 同一個帳號最大會話數 默認1
*/
private int maxSession = 1;
/**
* session管理器
*/
private SessionManager sessionManager;
/**
* 緩存管理器
*/
private Cache> 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("kickoutSession");
}
/**
* 是否允許訪問
*/
@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();
SysUserInfo object = (SysUserInfo) SecurityUtils.getSubject().getPrincipal();
Serializable sessionId = session.getId();
// 同步控制
Deque deque = cache.get(object.getAccount());
if (deque == null) {
deque = new LinkedList();
cache.put(object.getAccount(), 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();
}
// 否則踢出前者
else {
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null) {
// 設置會話的kickout屬性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
// 會話被踢出了
try {
subject.logout();
} catch (Exception e) {
e.printStackTrace();
}
saveRequest(request);
HttpServletRequest httpRequest = WebUtils.toHttp(request);
// 如果是ajax請求
if (isAjax(httpRequest)) {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 使得http會話過期
httpServletResponse.sendError(0);
return false;
} else {
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
}
return true;
}
/**
* 判斷是否為ajax請求
*
* @param request
* @return boolean對象
*/
public static boolean isAjax(ServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"));
}
}
這里我們使用了 ,我們需要在 shiro-.xml 配置文件中,加一個存儲對象,如下:
/**
* 并發登錄控制
*
* @return
*/
@Bean
public KickoutSessionFilter kickoutSessionControlFilter() {
KickoutSessionFilter kickoutSessionControlFilter = new KickoutSessionFilter();
// 用于根據會話ID,獲取會話進行踢出操作的;
kickoutSessionControlFilter.setSessionManager(sessionManager());
// 使用cacheManager獲取相應的cache來緩存用戶登錄的會話;用于保存用戶—會話之間的關系的;
kickoutSessionControlFilter.setCacheManager(ehCacheManager());
// 是否踢出后來登錄的,默認是false;即后者登錄的用戶踢出前者登錄的用戶;
kickoutSessionControlFilter.setKickoutAfter(false);
// 同一個用戶最大的會話數,默認1;比如2的意思是同一個用戶允許最多同時兩個人登錄;
kickoutSessionControlFilter.setMaxSession(1);
// 被踢出后重定向到的地址;
kickoutSessionControlFilter.setKickoutUrl("/login?kickout=1");
return kickoutSessionControlFilter;
}
其中用到的 管理器,和 管理器在之前的博客中都有講到,本次不再贅述
我們將踢出的用戶重定向到登錄界面,并攜帶參數
/**
* 地址過濾器
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 設置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 設置登錄url
shiroFilterFactoryBean.setLoginUrl("/login");
// 設置主頁url
shiroFilterFactoryBean.setSuccessUrl("/");
// 設置未授權的url
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// 自定義攔截器限制并發人數
LinkedHashMap filtersMap = new LinkedHashMap<>();
// 限制同一帳號同時在線的個數
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 注銷登錄
filterChainDefinitionMap.put("/loginOut", "logout");
// 開放登錄接口
filterChainDefinitionMap.put("/doLogin", "anon");
// 開放獲取登錄驗證碼接口
filterChainDefinitionMap.put("/kaptcha/**", "anon");
// 開放Api接口
filterChainDefinitionMap.put("/api/**", "anon");
// 開放微信接口
filterChainDefinitionMap.put("/weixin/**", "anon");
// 開放websocket接口
filterChainDefinitionMap.put("/websocket/**", "anon");
// 開放接口文檔
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/service-worker.js", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
// 開放靜態資源
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/layui/**", "anon");
filterChainDefinitionMap.put("/layuimini/**", "anon");
filterChainDefinitionMap.put("/module/**", "anon");
filterChainDefinitionMap.put("/upload/**", "anon");
// 其余url全部攔截,必須放在最后
filterChainDefinitionMap.put("/**", "kickout,user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
我們在 an 對象中注入了自定義過濾器java踢出已登錄用戶,并在最后的地址攔截規則中增加了 ,即使我們開啟了記住登錄功能,該用戶也會被踢下線
我們在登錄頁面中,需要獲取地址中是否有 參數
// 是否被擠下線
if(location.href.indexOf("kickout") > 0){
setTimeout(function () {
layNotify.notice({
title: "登錄提示",
type: "error",
message: '您的賬戶已在另一臺設備上登錄,如非本人操作,請立即修改密碼!'
});
}, 1000)
}
這樣后面登錄的用戶就會擠掉前面登錄用戶java踢出已登錄用戶,導致前面登錄的用戶被踢下線了
如您在閱讀中發現不足,歡迎留言!!!