SpringMVC + Shiro實現(xiàn)用戶踢出功能

整理自己的代碼片段的時候, 想起來之前有一個需求: 實現(xiàn)同一個用戶同時只能在一個地方登陸, 如果該用戶在其他地方登陸, 踢出前一個登陸狀態(tài)十绑。
項目中使用的是shiro做為權限控制框架, 對此需求進行了一些實現(xiàn), 思路如下: shiro是利用一個個filter進行過濾請求的權限, 那么我就可以自定義一個filter在用戶登陸之后, 判斷當前的用戶是否存在已經(jīng)登陸的情況, 如果已經(jīng)存在, 那么就踢出。


代碼分析

  1. 存儲用戶當前登陸的狀態(tài), 這里其實可以用redis, 但是因為項目比較小, 我直接使用了內(nèi)存Map來實現(xiàn);
/**
 * 當前登錄用戶session的信息
 *
 * @author videomonster
 */
public class SessionCacheHolder {

    /**
     * 用戶account, SessionId
     */
    public static Map<String, Serializable> loginSessionCache = Maps.newConcurrentMap();

    /**
     * session map
     * true: 踢出
     * false: 未踢出
     */
    public static Map<Serializable, Boolean> sessionStatusMap = Maps.newConcurrentMap();
}
  1. 定義一個KickoutSessionFilter, 這個filter繼承Shiro提供的AccessControlFilter, 其中有兩個方法需要我們復寫實現(xiàn), 一個是isAccessAllowed, 這個方法返回值是Boolean, 主要是判斷邏輯是否能通過, 如果不通過, 就會調(diào)用第二個方法onAccessDenied來處理, 并且AccessControlFilter提供了getSubject方法, 可以通過當前的request和response獲取當前主體(當前主體也可以通過Shiro提供的工具類SecurityUtils.getSubject()來獲取);
/**
 * 踢出重復登錄用戶
 *
 * @author videomonster
 */
@Slf4j
@Component
public class KickoutSessionFilter extends AccessControlFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request,
                                      ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        //如果是相關目錄或者是如果沒有登錄, 就直接return true
        if ((!subject.isAuthenticated() && !subject.isRemembered())) {
            return Boolean.TRUE;
        }
        // 獲取當前登錄的用戶的相關信息
        Session session = subject.getSession();
        Serializable sessionId = session.getId();
        //  判斷是否已經(jīng)踢出
        Boolean kickout = SessionCacheHolder.sessionStatusMap.get(sessionId);
        if (null != kickout && kickout) {
            // 移除該session數(shù)據(jù)
            SessionCacheHolder.sessionStatusMap.remove(sessionId);
            return Boolean.FALSE;
        }
        // 獲取mobileId
        User user = (User) subject.getPrincipal();
        String mobileId = user.getMobile();
        if (SessionCacheHolder.loginSessionCache.containsKey(mobileId)) {
            // 如果已經(jīng)包含當前Session,并且是同一個用戶悠瞬,跳過嘁灯。
            if (SessionCacheHolder.loginSessionCache.containsValue(sessionId)) {
                return Boolean.TRUE;
            }
            /*
             * 如果用戶Id相同, Session值不相同
             * 1.獲取到原來的session对碌,并且標記為踢出帝璧。
             * 2.繼續(xù)走
             */
            Serializable oldSessionId = SessionCacheHolder.loginSessionCache.get(mobileId);
            SessionCacheHolder.sessionStatusMap.put(oldSessionId, Boolean.TRUE);
            log.info("用戶手機號: {}, 姓名: {} 登陸, 當前sessionId: {}, 踢出 session id: {}",
                    mobileId, user.getRealname(), sessionId, oldSessionId);
        }
        SessionCacheHolder.loginSessionCache.put(mobileId, sessionId);
        // 當前session標記為未被踢出
        SessionCacheHolder.sessionStatusMap.put(sessionId, Boolean.FALSE);
        return Boolean.TRUE;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request,
                                     ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        // 先退出該用戶
        subject.logout();
        if (isAjaxRequest((HttpServletRequest) request)) {
            // 如果是ajax請求
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            Map<String, Object> rtn = Maps.newHashMap();
            rtn.put("code", 510);
            rtn.put("msg", "當前用戶已在其他地方登陸, 請重新登錄!");
            httpServletResponse.getWriter().write(JsonUtils.toJson(rtn));
        } else {
            // 重定向到指定位置
            WebUtils.issueRedirect(request, response, "/login.html");
        }
        return false;
    }

    private boolean isAjaxRequest(HttpServletRequest request) {
        String header = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equals(header);
    }
}
  1. 在shiro的配置文件中加入KickoutSessionFilter, 這里采用的是SpringMVC的XML配置模式。 需要注意的是, 因為這里只是想要實現(xiàn)判斷當前用戶是否已經(jīng)在其他地方登陸, 所以我們需要把filter放到過濾鏈中執(zhí)行真正進行鑒權的filter之前;
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <!-- Shiro的核心安全接口,這個屬性是必須的 -->
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.html"/>
    <!-- 登錄成功后要跳轉的連接 -->
    <property name="successUrl" value="/index.html"/>
    <property name="unauthorizedUrl" value="/404.html"/>
    <property name="filters">
      <util:map>
        <!-- 注冊kickoutSessionFilter -->
        <entry key="kickout" value-ref="kickoutSessionFilter"/>
      </util:map>
    </property>
    <property name="filterChainDefinitions">
      <value>
        /resources/**=anon
        /favicon.ico=anon
        /login.html=anon
        /logout.html=anon
        /captcha.jpg=anon
        /login=anon
        /=anon
        <!-- 注冊kickoutSessionFilter 到過濾鏈中, 并且放在真正鑒權處理的filter之前 -->
        /**=kickout,authc
      </value>
    </property>
  </bean>

效果展示

踢出用戶

本文只是記錄自己實現(xiàn)Shiro用戶踢出的一種方式, 歡迎各位來指正和提出建議拌阴。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绍绘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子迟赃,更是在濱河造成了極大的恐慌陪拘,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纤壁,死亡現(xiàn)場離奇詭異左刽,居然都是意外死亡,警方通過查閱死者的電腦和手機酌媒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門欠痴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秒咨,你說我怎么就攤上這事喇辽。” “怎么了雨席?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵菩咨,是天一觀的道長。 經(jīng)常有香客問我陡厘,道長抽米,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任雏亚,我火速辦了婚禮缨硝,結果婚禮上摩钙,老公的妹妹穿的比我還像新娘罢低。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布网持。 她就那樣靜靜地躺著宜岛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪功舀。 梳的紋絲不亂的頭發(fā)上萍倡,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機與錄音辟汰,去河邊找鬼列敲。 笑死,一個胖子當著我的面吹牛帖汞,可吹牛的內(nèi)容都是我干的戴而。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼翩蘸,長吁一口氣:“原來是場噩夢啊……” “哼所意!你這毒婦竟也來了?” 一聲冷哼從身側響起催首,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤扶踊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后郎任,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秧耗,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年涝滴,在試婚紗的時候發(fā)現(xiàn)自己被綠了绣版。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡歼疮,死狀恐怖杂抽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情韩脏,我是刑警寧澤缩麸,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站赡矢,受9級特大地震影響杭朱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吹散,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一弧械、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧空民,春花似錦刃唐、人聲如沸羞迷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衔瓮。三九已至,卻和暖如春抖甘,著一層夾襖步出監(jiān)牢的瞬間热鞍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工衔彻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留薇宠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓艰额,卻偏偏與公主長得像昼接,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子悴晰,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359