多账号登录控制

  • 2020 年 7 月 22 日
  • 筆記

多账号登录控制

场景:java系统中用户账号登录实现控制,实现用户同时只能在一处登录

思路

  1. 用户登录时添加用户的登录信息
  2. 用户退出时删除用户的登录信息
  3. 用户请求的session超时时,删除用户的登录信息
  4. 用户访问系统时,拦截请求查看是否与已保存的登录信息匹配

分析

思路图

主要代码:

登录页面部分代码:

//判断当前账号是否已经登录
$.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:/");
}