多帳號登錄控制
- 2020 年 7 月 22 日
- 筆記
多帳號登錄控制
場景:java系統中用戶帳號登錄實現控制,實現用戶同時只能在一處登錄
思路:
- 用戶登錄時添加用戶的登錄資訊
- 用戶退出時刪除用戶的登錄資訊
- 用戶請求的session超時時,刪除用戶的登錄資訊
- 用戶訪問系統時,攔截請求查看是否與已保存的登錄資訊匹配
分析:
思路圖

主要程式碼:
登錄頁面部分程式碼:
//判斷當前帳號是否已經登錄
$.get('${ROOT}/platform/isLogon', function (r) {
//已登錄,則增加提醒框,確認是否繼續登錄
if (r) {
confirm("當前帳號已登錄,繼續登錄會使其他登錄退出,確認繼續登錄嘛?", function () {
// 登錄處理
isProcessing = true;
$('#btnLogin').text('登錄中......').prop('disabled', 'true');
$.post
(
'${ROOT}/platform/logon', params,
function (rsp, textStatus, jqXHR) {
//登錄請求的處理邏輯
}
);
}, function () {
//取消繼續登錄,則返回false
return false;
});
//未登錄則繼續登錄
} else {
// 登錄處理
isProcessing = true;
$('#btnLogin').text('登錄中......').prop('disabled', 'true');
$.post
(
'${ROOT}/platform/logon', params,
function (rsp, textStatus, jqXHR) {
//登錄請求的處理邏輯
}
);
}
});
登錄時後台程式碼:
@RequestMapping(path = "/platform/isLogon", method = RequestMethod.GET)
public boolean isLogon(HttpServletRequest req) throws Exception
{
IForm form = getFormWrapper(req);
String loginId = form.get("userName");
SystemService service = GEOFF.SPRING_CONTEXT.getBean(SystemService.class);
User user = service.isLogon(loginId);
if (user != null && loginUserMap.size() >0){
String id = loginUserMap.get(user.getId().toString());
if (StringUtils.isNotBlank(id)){
lg.info("當前用戶:"+loginId+" 已登錄系統,增加登錄確認彈框...");
return true;
}
}
lg.info("當前用戶:"+loginId+" 未登錄系統,正常登錄系統...");
return false;
}
@RequestMapping(path = "/platform/logon", method = RequestMethod.POST)
public Object logon(HttpServletRequest req) throws Exception
{
/**
**
**登錄時帳號,密碼,驗證碼的校驗邏輯
**
*/
HttpSession session = req.getSession();
User sessionUser = (User) session.getAttribute("SESSION_USER");
if (StringUtils.isNotBlank(session.getId()) && sessionUser != null) {
loginUserMap.put(sessionUser.getId().toString(), session.getId());
}else{
lg.error("進入保存用戶登錄資訊到全局變數方法中,保存用戶登錄資訊出錯!!!");
}
Map<String, Object> rejson = new HashMap<>();
rejson.put("success", true);
rejson.put("token", token);
return JSON.toJSONString(rejson);
}
定義保存用戶登錄的全局變數:
import java.util.HashMap;
import java.util.Map;
public class Geoff
{
//=================================================================
// 全局常量
//=================================================================
/**
* 存儲在線用戶,控制用戶帳號登錄
*/
public static Map<String, String> loginUserMap = new HashMap<>();
}
用戶訪問系統時的校驗:
package cn.geoff.framework.core;
import cn.geoff.framework.core.controller.BaseController;
import cn.geoff.framework.core.vo.InterfaceMessage;
import cn.geoff.framework.core.vo.Message;
import cn.geoff.framework.platform.vo.User;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import static cn.geoff.framework.core.FORP.loginUserMap;
/**
* @author: Geoff
* @create: 2020-07-21 14:45
* @description:
*/
public class LoginInitInterceptor extends HandlerInterceptorAdapter {
private static final Logger lg = LoggerFactory.getLogger(LoginInitInterceptor.class);
/**
* 攔截系統的請求,實現多帳號登錄控制
* @Author: Geoff
* @date: 2020/7/22 11:13
* @param: request
* @param response
* @param handler
* @return: boolean
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
lg.info("請求到達Interceptor攔截器,開始處理攔截邏輯...");
try {
//獲取請求中的session和用戶對象
HttpSession session = request.getSession();
User sessionUser = (User) session.getAttribute(FORP.SESSION_USER);
if (loginUserMap != null && loginUserMap.size() > 0 && sessionUser != null) {
String sessionID = loginUserMap.get(sessionUser.getId().toString());
//進行非空校驗,如何請求session與全局資訊中不一致,則清除快取,並跳轉到登錄頁面
if (StringUtils.isNotBlank(sessionID)) {
if (sessionID.equalsIgnoreCase(session.getId())) {
//兩處sessionID一致則不做處理
lg.info("Interceptor攔截器攔截請求中的sessionID與全局登錄Map中一致!!!");
return true;
}else{
//不一致則清除快取
lg.info("Interceptor攔截器攔截請求中的sessionID與全局登錄Map中【不】一致,"+sessionUser.getUserName()+"強制退出系統!");
handleSessionTimeout(request,response);
return false;
}
}
}
} catch (Exception e) {
lg.error("Interceptor攔截器攔截請求出錯!!!");
}
return false;
}
private void handleSessionTimeout(HttpServletRequest req, HttpServletResponse resp) throws Exception {
lg.warn("Session已超時,重定向至登陸頁面:{}", req.getRequestURL());
lg.debug("Referer:{}", req.getHeader("Referer"));
if (BaseController.isAjaxRequest(req)) {
InterfaceMessage message = new InterfaceMessage(401, "請求失敗:無效的登錄資訊!");
BaseController.writeJsonResponse(message, resp);
} else {
Message message = new Message("您的帳號已在其它地方登錄,如非本人操作,請及時修改密碼!");
message.setType(3);
req.setAttribute("msg", message);
req.setAttribute("actionURL", "/");
req.getRequestDispatcher("/WEB-INF/jsp/common/message.jsp").forward(req, resp);
}
}
}
對應的配置文件:
<!--多帳號登錄控制攔截器-->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<!--排查登錄介面攔截-->
<mvc:exclude-mapping path="/"/>
<mvc:exclude-mapping path="/platform/isLogon"/>
<mvc:exclude-mapping path="/platform/logon"/>
<mvc:exclude-mapping path="/platform/error/**"/>
<!--排除靜態資源請求-->
<mvc:exclude-mapping path="/favicon.ico"/>
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/css/**"/>
<mvc:exclude-mapping path="/fonts/**"/>
<mvc:exclude-mapping path="/image/**"/>
<mvc:exclude-mapping path="/images/**"/>
<mvc:exclude-mapping path="/app/**"/>
<mvc:exclude-mapping path="/disk-file/**"/>
<mvc:exclude-mapping path="/sound/**"/>
<mvc:exclude-mapping path="/geoff/**"/>
<bean class="cn.geoff.framework.core.LoginInitInterceptor"/>
</mvc:interceptor>
session超時後,清除用戶的登錄資訊:
package cn.geoff.framework.core;
import cn.geoff.framework.core.util.Redis;
import cn.geoff.framework.platform.vo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import static cn.geoff.framework.core.GEOFF.loginUserMap;
/**
* session監聽器,監聽session的創建與銷毀
* @author: Geoff
* @create: 2020-07-21 09:42
* @description:
*/
public class SessionListener implements HttpSessionListener {
private static final Logger lg = LoggerFactory.getLogger(SessionListener.class);
/**
* 實現session介面,監聽session創建的方法
* @Author: Geoff
* @date: 2020/7/21 9:43
* @param: httpSessionEvent
* @return: void
*/
@Override
public void sessionCreated(HttpSessionEvent event) {
lg.info("session監聽器初始化載入......");
}
/**
* 實現session介面,監聽session的銷毀方法
* @Author: Geoff
* @date: 2020/7/21 9:44
* @param: httpSessionEvent
* @return: void
*/
@Override
public void sessionDestroyed(HttpSessionEvent event) {
HttpSession session = event.getSession();
//獲取當前session中用戶
User sessionUser = (User)session.getAttribute(GEOFF.SESSION_USER);
//如果用戶不為空,銷毀對應的session
if (sessionUser != null){
try {
//清除session中對用的用戶資訊
session.removeAttribute(GEOFF.SESSION_USER);
//獲取當前的token
String workToken = (String)session.getAttribute("workToken");
//清除Redis中用戶的登錄資訊
Redis.delete(workToken,null);
//清除整個session資訊
session.invalidate();
}catch (Exception e){
lg.error(sessionUser.getUserName()+":用戶對應session超時後,銷毀session資訊出錯!");
}finally {
//清除已保存的用戶登錄資訊
loginUserMap.remove(sessionUser.getId().toString());
}
}
}
}
對應配置文件:
<!--多帳號控制session監聽器-->
<listener>
<listener-class>cn.geoff.framework.core.SessionListener</listener-class>
</listener>
用戶退出系統時:
/**
* 退出系統
* @param id 用戶ID
* @param req 請求參數
*/
@RequestMapping("/platform/logout/{id}")
public ModelAndView logout(@PathVariable Long id, HttpServletRequest req)
{
lg.info("退出系統:userId[{}]", id);
// 清除用戶許可權緩衝
User user = getSessionUser(req);
//清除session中用戶資訊,並將session初始
req.getSession().removeAttribute(GEOFF.SESSION_USER);
req.getSession().invalidate();
//清除已保存的用戶登錄資訊
loginUserMap.remove(user.getId().toString());
// 返回首頁
return new ModelAndView("redirect:/");
}

