【实践】使用session实现单用户多端登录限制

1. 摘要

软件设计中,经常存在这样的场景,为了防止计费等冲突限制,实现同一个用户不允许同一个用户多个设备同时登录,只允许唯一登录。本文介绍实现方法。

2.设计场景

1)同一时刻不允许某个用户多地登录。
2)用户已在A处登录,现在从B处登录是允许的,但会把A处挤掉(考虑到用户在A处登录后因某些情况跑到了B处,但还想继续之前的工作,所以需要登录系统)。
3)B处挤掉A后,A再做其它操作的时候系统会给出提示,该用户在别处登录,如不是本人操作可能密码泄漏,请修改密码。

3. 业务流程图

每个用户登录的时候,通常我们会将用户信息存入session,以便用户进行操作的时候系统方便得到用户的基本信息。但这个session具有私有性,只对当前用户可见(如果同意用户在不同浏览器登录会得到不同的session,这也是为什么可以多用户登录的根源所在)。那么接着问题就来了,某个用户登录的时候如何能知道自己是否在线,相信聪明的你已经想到,这还不好半,把在线的用户信息存储在一个公共的地方问题不就迎刃而解了么,网上一查,解决方案无出其右,大致为以下两种:
  1)数据库中标识在线用户
  2)存储到application中

经过重重考虑,我们会发现方案一需要解决许多棘手的问题(用户异常退出未来得及修改状态,频繁访问数据库影响性能等),这对于一个要求完美的你来说显然是不合时宜的,于是我们采用了方案二,将在线用户信息保存到application中,具体设计如下。

3.1 登录流程图 -B处登录

3.1被挤掉后操作流程图 -A处已登录

3. 代码实现

1)登录方法

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(String userName, String password, RedirectAttributes redirectAttributes, HttpServletRequest request) {
        //判断用户是否已经在线及处理(已在线则剔除)
        String loginLimite = limiteLogin.loginLimite(request, userName);
        //判断用户名、密码是否正确
        String result = userService.login(userName, password);
        if (result.equals("success")) {
            request.getSession().setAttribute("now_user", userService.findByUserName(userName));        //用户掉线,登录后重定向到保存的链接
            Object url = request.getSession().getAttribute("redirect_link");
            if (url != null) {
                request.getSession().removeAttribute("redirect_link");
                return "redirect:" + url.toString();
            }
            return "index";
        }
        redirectAttributes.addFlashAttribute("message", result);
        return "redirect:/other/toLogin";
    }

2)登录判断是否已经在线

@Service
@Transactional
public class LimiteLogin {

    private static Logger log = Logger.getLogger(SessionListener.class);

    private static Map<String, String> loginUserMap = new HashMap<>();//存储在线用户
    private static Map<String, String> loginOutTime = new HashMap<>();//存储剔除用户时间
    @Autowired
    private UserService userService;

    public String loginLimite(HttpServletRequest request, String userName) {
        User user = userService.findByUserName(userName);
        String sessionId = request.getSession().getId();
        for (String key : loginUserMap.keySet()) {
            //用户已在另一处登录
            if (key.equals(user.getUserName()) && !loginUserMap.containsValue(sessionId)) {
                log.info("用户:" + user.getUserName() + ",于" + DateUtil.dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss") + "被剔除!");
                loginOutTime.put(user.getUserName(), DateUtil.dateFormat(new Date(), "yyyy-MM-dd HH:mm:ss"));
                loginUserMap.remove(user.getUserName());
                break;
            }
        }

        loginUserMap.put(user.getUserName(), sessionId);
        request.getSession().getServletContext().setAttribute("loginUserMap", loginUserMap);
        request.getSession().getServletContext().setAttribute("loginOutTime", loginOutTime);
        return "success";
    }


}

3)登录拦截器(未登录跳转登录页)

public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("now_user");
        if (session.getAttribute("now_user") == null) {
            response.sendRedirect(request.getContextPath() + "/other/toLogin");
            return false;
        }

        //多用户登录限制判断,并给出提示信息
        boolean isLogin = false;
        if (user != null) {
            Map<String, String> loginUserMap = (Map<String, String>) session.getServletContext().getAttribute("loginUserMap");
            String sessionId = session.getId();
            for (String key : loginUserMap.keySet()) {
                //用户已在另一处登录
                if (key.equals(user.getUserName()) && !loginUserMap.containsValue(sessionId)) {
                    isLogin = true;
                    break;
                }
            }
        }
        if (isLogin) {
            Map<String, String> loginOutTime = (Map<String, String>) session.getServletContext().getAttribute("loginOutTime");
            session.setAttribute("mess", "用户:" + user.getUserName() + ",于 " + loginOutTime.get(user.getUserName()) + " 已在别处登录!");
            loginOutTime.remove(user.getUserName());
            session.getServletContext().setAttribute("loginUserMap", loginOutTime);
            response.sendRedirect(request.getContextPath() + "/other/toLogin");
            return false;
        }

        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }
}

4)在session销毁的时候,把loginUserMap中保存的键值对清除

public class SessionListener implements HttpSessionListener {

    private static Logger log = Logger.getLogger(SessionListener.class);

    @Override
    public void sessionCreated(HttpSessionEvent event) {

    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        HttpSession session = event.getSession();
        String sessionId = session.getId();
        //在session销毁的时候,把loginUserMap中保存的键值对清除
        User user = (User) session.getAttribute("now_user");
        if (user != null) {
            Map<String, String> loginUserMap = (Map<String, String>) event.getSession().getServletContext().getAttribute("loginUserMap");
            if(loginUserMap.get(user.getUserName()).equals(sessionId)){
                log.info("clean user from application : " + user.getUserName());
                loginUserMap.remove(user.getUserName());
                event.getSession().getServletContext().setAttribute("loginUserMap", loginUserMap);
            }
        }

    }

}

5)web.xml

<!-- session listener 多用户登录限制,退出清除session信息的同时清除application中存放用户登录信息-->
  <listener>
    <listener-class>com.service.limitelogin.SessionListener</listener-class>
  </listener>

6)页面代码

(用于给出提示的同时,清除被挤掉用户的session信息,否则提示信息会一直显示)

<script type="text/javascript">
    $(document).ready(function () {
        var message='${mess}';
        if (message != "") {
            $.ajax({
                       type: 'GET',
                       async: false,
                       cache: false,
                       url: '/other/clearUserSession',
                       dataType: '',
                       data: {},
                       success: function (data) {
                       }
                   });
            $('#mess').html(message);
        }
    });
</script>

7)清除挤掉用户session代码

/**
     * 多用户登录限制,清除session信息(登录信息、提示信息)
     *
     * @param request
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/clearUserSession")
    public String clearUserSession(HttpServletRequest request) {
        HttpSession httpSession = request.getSession();
        //httpSession.invalidate();
        httpSession.removeAttribute("now_user");
        httpSession.removeAttribute("mess");
        return "success";
    }

到此开发工作完成。

8)运行结果

4. 参考

  1. 代码下载:login_limit

  2. java web项目防止多用户重复登录解决方案

  3. 多用户登录限制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容