【實踐】使用session實現單用戶多端登錄限制

1. 摘要

軟件設計中其障,經常存在這樣的場景掐暮,為了防止計費等沖突限制,實現同一個用戶不允許同一個用戶多個設備同時登錄愿卒,只允許唯一登錄缚去。本文介紹實現方法。

2.設計場景

1)同一時刻不允許某個用戶多地登錄琼开。
2)用戶已在A處登錄易结,現在從B處登錄是允許的,但會把A處擠掉(考慮到用戶在A處登錄后因某些情況跑到了B處柜候,但還想繼續(xù)之前的工作搞动,所以需要登錄系統(tǒng))。
3)B處擠掉A后改橘,A再做其它操作的時候系統(tǒng)會給出提示滋尉,該用戶在別處登錄,如不是本人操作可能密碼泄漏飞主,請修改密碼狮惜。

3. 業(yè)務流程圖

每個用戶登錄的時候,通常我們會將用戶信息存入session碌识,以便用戶進行操作的時候系統(tǒng)方便得到用戶的基本信息碾篡。但這個session具有私有性,只對當前用戶可見(如果同意用戶在不同瀏覽器登錄會得到不同的session筏餐,這也是為什么可以多用戶登錄的根源所在)开泽。那么接著問題就來了,某個用戶登錄的時候如何能知道自己是否在線魁瞪,相信聰明的你已經想到穆律,這還不好半惠呼,把在線的用戶信息存儲在一個公共的地方問題不就迎刃而解了么,網上一查峦耘,解決方案無出其右剔蹋,大致為以下兩種:
  1)數據庫中標識在線用戶
  2)存儲到application中

經過重重考慮,我們會發(fā)現方案一需要解決許多棘手的問題(用戶異常退出未來得及修改狀態(tài)辅髓,頻繁訪問數據庫影響性能等)泣崩,這對于一個要求完美的你來說顯然是不合時宜的,于是我們采用了方案二洛口,將在線用戶信息保存到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";
    }

到此開發(fā)工作完成。

8)運行結果

4. 參考

  1. 代碼下載:login_limit

  2. java web項目防止多用戶重復登錄解決方案

  3. 多用戶登錄限制

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末瞻佛,一起剝皮案震驚了整個濱河市脱篙,隨后出現的幾起案子,更是在濱河造成了極大的恐慌伤柄,老刑警劉巖绊困,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異适刀,居然都是意外死亡秤朗,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門笔喉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來取视,“玉大人,你說我怎么就攤上這事常挚∽魈罚” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵奄毡,是天一觀的道長折欠。 經常有香客問我,道長,這世上最難降的妖魔是什么锐秦? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任咪奖,我火速辦了婚禮,結果婚禮上农猬,老公的妹妹穿的比我還像新娘赡艰。我一直安慰自己售淡,他們只是感情好斤葱,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揖闸,像睡著了一般揍堕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汤纸,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天衩茸,我揣著相機與錄音,去河邊找鬼贮泞。 笑死楞慈,一個胖子當著我的面吹牛,可吹牛的內容都是我干的啃擦。 我是一名探鬼主播囊蓝,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼令蛉!你這毒婦竟也來了聚霜?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤珠叔,失蹤者是張志新(化名)和其女友劉穎蝎宇,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體祷安,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡姥芥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了汇鞭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凉唐。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖虱咧,靈堂內的尸體忽然破棺而出熊榛,到底是詐尸還是另有隱情,我是刑警寧澤腕巡,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布玄坦,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏煎楣。R本人自食惡果不足惜豺总,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望择懂。 院中可真熱鬧喻喳,春花似錦、人聲如沸困曙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慷丽。三九已至蹦哼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間要糊,已是汗流浹背纲熏。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留锄俄,地道東北人局劲。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像奶赠,于是被迫代替她去往敵國和親鱼填。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容