多賬號登入控制
場景: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:/");
}