溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

springboot+shiro之登錄人數(shù)限制、登錄判斷重定向、session時(shí)間設(shè)置的示例分析

發(fā)布時(shí)間:2021-11-22 15:01:49 來源:億速云 閱讀:241 作者:小新 欄目:開發(fā)技術(shù)

這篇文章將為大家詳細(xì)講解有關(guān)springboot+shiro之登錄人數(shù)限制、登錄判斷重定向、session時(shí)間設(shè)置的示例分析,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

springboot + shiro之登錄人數(shù)控制

項(xiàng)目

前篇:spring boot + mybatis + layui + shiro后臺(tái)權(quán)限管理系統(tǒng):https://blog.51cto.com/wyait/2082803

本文是基于spring boot + mybatis + layui + shiro后臺(tái)權(quán)限管理系統(tǒng)開發(fā)的,新增功能:

  1. shiro并發(fā)登陸人數(shù)控制(超出登錄用戶最大配置數(shù)量,清理用戶)功能;

  2. 解決在父子頁面中,判斷用戶未登錄之后,重定向到登錄頁面嵌套顯示問題;

  3. 解決ajax請(qǐng)求,判斷用戶未登錄之后,如何重定向到登錄頁面問題;

  4. 解決使用并完成了功能1,導(dǎo)致的session有效時(shí)間沖突問題。

后篇:

  • springboot + shiro 動(dòng)態(tài)更新用戶信息:https://blog.51cto.com/wyait/2112200

  • springboot + shiro 權(quán)限注解、統(tǒng)一異常處理、請(qǐng)求亂碼解決 :https://blog.51cto.com/wyait/2125708

項(xiàng)目源碼

項(xiàng)目源碼:(包含數(shù)據(jù)庫源碼)  
github源碼: https://github.com/wyait/manage.git  
碼云:https://gitee.com/wyait/manage.git  
github對(duì)應(yīng)項(xiàng)目源碼目錄:wyait-manage-1.2.0  
碼云對(duì)應(yīng)項(xiàng)目源碼目錄:wyait-manage-1.2.0

場(chǎng)景

同一個(gè)用戶,先在A×××登錄;之后在B×××登錄時(shí),退出A×××的登錄狀態(tài);反之相同?;蛘呦拗仆粋€(gè)用戶在不同的設(shè)備上,同時(shí)在線的數(shù)量;

技術(shù)實(shí)現(xiàn)

基于shiro和ehcache實(shí)現(xiàn)

解決思路

spring security就直接提供了相應(yīng)的功能;  
Shiro的話沒有提供默認(rèn)實(shí)現(xiàn),不過可以在Shiro中加入這個(gè)功能。就是使用shiro強(qiáng)大的自定義訪問控制攔截器:AccessControlFilter,集成這個(gè)接口后要實(shí)現(xiàn)下面這2個(gè)方法。

    /**
     * Returns <code>true</code> if the request is allowed to proceed through the filter normally, or <code>false</code>
     * if the request should be handled by the
     * {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(request,response,mappedValue)}
     * method instead.
     *
     * @param request     the incoming <code>ServletRequest</code>
     * @param response    the outgoing <code>ServletResponse</code>
     * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
     * @return <code>true</code> if the request should proceed through the filter normally, <code>false</code> if the
     *         request should be processed by this filter's
     *         {@link #onAccessDenied(ServletRequest,ServletResponse,Object)} method instead.
     * @throws Exception if an error occurs during processing.
     */
    protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;

    ... ...
    /**
     * Processes requests where the subject was denied access as determined by the
     * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed}
     * method.
     *
     * @param request  the incoming <code>ServletRequest</code>
     * @param response the outgoing <code>ServletResponse</code>
     * @return <code>true</code> if the request should continue to be processed; false if the subclass will
     *         handle/render the response directly.
     * @throws Exception if there is an error processing the request.
     */
    protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;

查看抽象類AccessControlFilter:

  • isAccessAllowed:表示是否允許訪問;mappedValue就是[urls]配置中攔截器參數(shù)部分,如果允許訪問返回true,否則false;

  • onAccessDenied:表示當(dāng)訪問拒絕時(shí)是否已經(jīng)處理了;如果返回true表示需要繼續(xù)處理;如果返回false表示該攔截器實(shí)例已經(jīng)處理了,將直接返回即可。

  • onPreHandle:會(huì)自動(dòng)調(diào)用這兩個(gè)方法決定是否繼續(xù)處理;

另外AccessControlFilter還提供了如下方法用于處理如登錄成功后/重定向到上一個(gè)請(qǐng)求:  
springboot+shiro之登錄人數(shù)限制、登錄判斷重定向、session時(shí)間設(shè)置的示例分析    

void setLoginUrl(String loginUrl) //身份驗(yàn)證時(shí)使用,默認(rèn)/login.jsp  
String getLoginUrl()  
Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject實(shí)例  
boolean isLoginRequest(ServletRequest request, ServletResponse response)//當(dāng)前請(qǐng)求是否是登錄請(qǐng)求  
void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當(dāng)前請(qǐng)求保存起來并重定向到登錄頁面  
void saveRequest(ServletRequest request) //將請(qǐng)求保存起來,如登錄成功后再重定向回該請(qǐng)求  
void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登錄頁面

要進(jìn)行用戶訪問控制,可以繼承AccessControlFilter。

  • 思路:  
    a. 登陸成功時(shí)將用戶保存到了shiro提供的session中,并同時(shí)添加到ehcache緩存中;  
    b. KickoutSessionFilter拿到了session之后先判斷能不能通過緩存取到值,如果取得到再和服務(wù)器端session進(jìn)行匹配(用戶的名字(每個(gè)用戶的名字必須不同));  
    c. 如果匹配,系統(tǒng)會(huì)為新登錄的用戶新建一個(gè)session;之前的session確認(rèn)失效并踢出,老用戶就無法繼續(xù)操作而被迫下線;

shiro技術(shù)實(shí)現(xiàn)流程

下面就是自定義的訪問控制攔截器:KickoutSessionFilter:

自定義過濾器類KickoutSessionFilter
package com.wyait.manage.filter;

import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Deque;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import com.wyait.manage.pojo.User;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.lyd.admin.pojo.AdminUser;

/**
 * 
 * @項(xiàng)目名稱:wyait-manager
 * @類名稱:KickoutSessionFilter
 * @類描述:自定義過濾器,進(jìn)行用戶訪問控制
 * @創(chuàng)建人:wyait
 * @創(chuàng)建時(shí)間:2018年4月24日 下午5:18:29
 * @version:
 */
public class KickoutSessionFilter extends AccessControlFilter {

    private static final Logger logger = LoggerFactory
            .getLogger(KickoutSessionFilter.class);

    private String kickoutUrl; // 踢出后到的地址
    private boolean kickoutAfter = false; // 踢出之前登錄的/之后登錄的用戶 默認(rèn)false踢出之前登錄的用戶
    private int maxSession = 1; // 同一個(gè)帳號(hào)最大會(huì)話數(shù) 默認(rèn)1
    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    // 設(shè)置Cache的key的前綴
    public void setCacheManager(CacheManager cacheManager) {
        //必須和ehcache緩存配置中的緩存name一致
        this.cache = cacheManager.getCache("shiro-activeSessionCache");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request,
            ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request,
            ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        // 沒有登錄授權(quán) 且沒有記住我
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            // 如果沒有登錄,直接進(jìn)行之后的流程
            return true;
        }
        Session session = subject.getSession();
        logger.debug("==session時(shí)間設(shè)置:" + String.valueOf(session.getTimeout())
                + "===========");
        try {
            // 當(dāng)前用戶
            User user = (User) subject.getPrincipal();
            String username = user.getUsername();
            logger.debug("===當(dāng)前用戶username:==" + username);
            Serializable sessionId = session.getId();
            logger.debug("===當(dāng)前用戶sessionId:==" + sessionId);
            // 讀取緩存用戶 沒有就存入
            Deque<Serializable> deque = cache.get(username);
            logger.debug("===當(dāng)前deque:==" + deque);
            if (deque == null) {
                // 初始化隊(duì)列
                deque = new ArrayDeque<Serializable>();
            }
            // 如果隊(duì)列里沒有此sessionId,且用戶沒有被踢出;放入隊(duì)列
            if (!deque.contains(sessionId)
                    && session.getAttribute("kickout") == null) {
                // 將sessionId存入隊(duì)列
                deque.push(sessionId);
                // 將用戶的sessionId隊(duì)列緩存
                cache.put(username, deque);
            }
            // 如果隊(duì)列里的sessionId數(shù)超出最大會(huì)話數(shù),開始踢人
            while (deque.size() > maxSession) {
                logger.debug("===deque隊(duì)列長(zhǎng)度:==" + deque.size());
                Serializable kickoutSessionId = null;
                // 是否踢出后來登錄的,默認(rèn)是false;即后者登錄的用戶踢出前者登錄的用戶;
                if (kickoutAfter) { // 如果踢出后者
                    kickoutSessionId = deque.removeFirst();
                } else { // 否則踢出前者
                    kickoutSessionId = deque.removeLast();
                }
                // 踢出后再更新下緩存隊(duì)列
                cache.put(username, deque);
                try {
                    // 獲取被踢出的sessionId的session對(duì)象
                    Session kickoutSession = sessionManager
                            .getSession(new DefaultSessionKey(kickoutSessionId));
                    if (kickoutSession != null) {
                        // 設(shè)置會(huì)話的kickout屬性表示踢出了
                        kickoutSession.setAttribute("kickout", true);
                    }
                } catch (Exception e) {// ignore exception
                }
            }
            // ajax請(qǐng)求

            // 如果被踢出了,(前者或后者)直接退出,重定向到踢出后的地址
            if ((Boolean) session.getAttribute("kickout") != null
                    && (Boolean) session.getAttribute("kickout") == true) {
                // 會(huì)話被踢出了
                try {
                    // 退出登錄
                    subject.logout();
                } catch (Exception e) { // ignore
                }
                saveRequest(request);
                logger.debug("==踢出后用戶重定向的路徑kickoutUrl:" + kickoutUrl);
                // 重定向
                WebUtils.issueRedirect(request, response, kickoutUrl);
                return false;
            }
            return true;
        } catch (Exception e) { // ignore
            //重定向到登錄界面
            WebUtils.issueRedirect(request, response, "/login");
            return false;
        }
    }

}
設(shè)置ShiroConfig配置類
  • SessionDAO  用于會(huì)話的CRUD;查看該接口源碼:

public interface SessionDAO {  
    /*如DefaultSessionManager在創(chuàng)建完session后會(huì)調(diào)用該方法; 
      如保存到關(guān)系數(shù)據(jù)庫/文件系統(tǒng)/NoSQL數(shù)據(jù)庫;即可以實(shí)現(xiàn)會(huì)話的持久化; 
      返回會(huì)話ID;主要此處返回的ID.equals(session.getId());   
    */    
    Serializable create(Session session);    

    //根據(jù)會(huì)話ID獲取會(huì)話    
    Session readSession(Serializable sessionId) throws UnknownSessionException;    

    //更新會(huì)話;如更新會(huì)話最后訪問時(shí)間/停止會(huì)話/設(shè)置超時(shí)時(shí)間/設(shè)置移除屬性等會(huì)調(diào)用    
    void update(Session session) throws UnknownSessionException;    

    //刪除會(huì)話;當(dāng)會(huì)話過期/會(huì)話停止(如用戶退出時(shí))會(huì)調(diào)用    
    void delete(Session session);    

    //獲取當(dāng)前所有活躍用戶,如果用戶量多此方法影響性能    
    Collection<Session> getActiveSessions();     
}

SessionDAO實(shí)現(xiàn)類:

a. AbstractSessionDAO提供了SessionDAO的基礎(chǔ)實(shí)現(xiàn),如生成會(huì)話ID等;  
b. CachingSessionDAO提供了對(duì)開發(fā)者透明的會(huì)話緩存的功能,只需要設(shè)置相應(yīng)的CacheManager即可;  
c. MemorySessionDAO直接在內(nèi)存中進(jìn)行會(huì)話維護(hù);  
d. EnterpriseCacheSessionDAO提供了緩存功能的會(huì)話維護(hù),默認(rèn)情況下使用MapCache實(shí)現(xiàn),內(nèi)部使用ConcurrentHashMap保存緩存的會(huì)話。
  1. ShiroConfig配置類中EnterpriseCacheSessionDAO配置:

    /**
     * EnterpriseCacheSessionDAO shiro sessionDao層的實(shí)現(xiàn);
     * 提供了緩存功能的會(huì)話維護(hù),默認(rèn)情況下使用MapCache實(shí)現(xiàn),內(nèi)部使用ConcurrentHashMap保存緩存的會(huì)話。
     */
    @Bean
    public EnterpriseCacheSessionDAO enterCacheSessionDAO() {
        EnterpriseCacheSessionDAO enterCacheSessionDAO = new EnterpriseCacheSessionDAO();
        //添加緩存管理器
        //enterCacheSessionDAO.setCacheManager(ehCacheManager());
        //添加ehcache活躍緩存名稱(必須和ehcache緩存名稱一致)
        enterCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
        return enterCacheSessionDAO;
    }
  2. SessionManager配置:

/**
     *
     * @描述:sessionManager添加session緩存操作DAO
     * @創(chuàng)建人:wyait
     * @創(chuàng)建時(shí)間:2018年4月24日 下午8:13:52
     * @return
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //sessionManager.setCacheManager(ehCacheManager());
        sessionManager.setSessionDAO(enterCacheSessionDAO());
        return sessionManager;
    }
  1. kickoutSessionFilter配置

/**
     *
     * @描述:kickoutSessionFilter同一個(gè)用戶多設(shè)備登錄限制
     * @創(chuàng)建人:wyait
     * @創(chuàng)建時(shí)間:2018年4月24日 下午8:14:28
     * @return
     */
    public KickoutSessionFilter kickoutSessionFilter(){
        KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
        //使用cacheManager獲取相應(yīng)的cache來緩存用戶登錄的會(huì)話;用于保存用戶—會(huì)話之間的關(guān)系的;
        //這里我們還是用之前shiro使用的ehcache實(shí)現(xiàn)的cacheManager()緩存管理
        //也可以重新另寫一個(gè),重新配置緩存時(shí)間之類的自定義緩存屬性
        kickoutSessionFilter.setCacheManager(ehCacheManager());
        //用于根據(jù)會(huì)話ID,獲取會(huì)話進(jìn)行踢出操作的;
        kickoutSessionFilter.setSessionManager(sessionManager());
        //是否踢出后來登錄的,默認(rèn)是false;即后者登錄的用戶踢出前者登錄的用戶;踢出順序。
        kickoutSessionFilter.setKickoutAfter(false);
        //同一個(gè)用戶最大的會(huì)話數(shù),默認(rèn)1;比如2的意思是同一個(gè)用戶允許最多同時(shí)兩個(gè)人登錄;
        kickoutSessionFilter.setMaxSession(1);
        //被踢出后重定向到的地址;
        kickoutSessionFilter.setKickoutUrl("/toLogin?kickout=1");
        return kickoutSessionFilter;
    }
  1. 將SessionManager交給SecurityManager管理

/**
     * shiro安全管理器設(shè)置realm認(rèn)證、ehcache緩存管理、session管理器、Cookie記住我管理器
     * @return
     */
    @Bean public org.apache.shiro.mgt.SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設(shè)置realm.
        securityManager.setRealm(shiroRealm());
        // //注入ehcache緩存管理器;
        securityManager.setCacheManager(ehCacheManager());
        // //注入session管理器;
        securityManager.setSessionManager(sessionManager());
        //注入Cookie記住我管理器
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }
  1. 配置filterChainDefinitionMap

...
//添加kickout認(rèn)證
HashMap<String,Filter> hashMap=new HashMap<String,Filter>();
hashMap.put("kickout",kickoutSessionFilter());
shiroFilterFactoryBean.setFilters(hashMap);
...
filterChainDefinitionMap.put("/**", "kickout,authc");
...

解決子頁面,重定向之后,出現(xiàn)頁面嵌套的問題

新增登錄中轉(zhuǎn)頁面toLogin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!--head部分-->
<head th:include="layout :: htmlhead" th:with="title='利易達(dá)貸款后臺(tái)'">
</head>
<script type="text/javascript">
var href=window.location.href;
if(href.indexOf("kickout")>0){
    setTimeout("top.location.href='/login?kickout';", 0); 
}else{
    setTimeout("top.location.href='/login';", 0); 
}
</script>
</html>
更改shiro中filterChainDefinitionMap配置
// 指定要求登錄時(shí)的鏈接
shiroFilterFactoryBean.setLoginUrl("/toLogin");
...
// 配置不會(huì)被攔截的鏈接 從上向下順序判斷
filterChainDefinitionMap.put("/login", "anon");

上面兩個(gè)配置,即可解決頁面重定向后,嵌套問題。

ajax請(qǐng)求問題

如果對(duì)用戶在線數(shù)量進(jìn)行限制,踢出了之前登錄的用戶A;這時(shí)候用戶A在系統(tǒng)中,發(fā)送了一個(gè)ajax請(qǐng)求,會(huì)出現(xiàn)彈框空白等問題;

解決方案
  1. 自定義ShiroFilterUtils工具類判斷請(qǐng)求是否為ajax

package com.wyait.manage.utils;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * 
 * @項(xiàng)目名稱:wyait-manager
 * @類名稱:ShiroFilterUtils
 * @類描述:shiro工具類
 * @創(chuàng)建人:wyait
 * @創(chuàng)建時(shí)間:2018年4月24日 下午5:12:04 
 * @version:
 */
public class ShiroFilterUtils {
    private static final Logger logger = LoggerFactory
            .getLogger(ShiroFilterUtils.class);
    /**
     * 
     * @描述:判斷請(qǐng)求是否是ajax
     * @創(chuàng)建人:wyait
     * @創(chuàng)建時(shí)間:2018年4月24日 下午5:00:22
     * @param request
     * @return
     */
    public static boolean isAjax(ServletRequest request){
        String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
        if("XMLHttpRequest".equalsIgnoreCase(header)){
            logger.debug("shiro工具類【wyait-manager-->ShiroFilterUtils.isAjax】當(dāng)前請(qǐng)求,為Ajax請(qǐng)求");
            return Boolean.TRUE;
        }
        logger.debug("shiro工具類【wyait-manager-->ShiroFilterUtils.isAjax】當(dāng)前請(qǐng)求,非Ajax請(qǐng)求");
        return Boolean.FALSE;
    }
}
  1. 調(diào)整KickoutSessionFilter過濾器,新增ajax請(qǐng)求判斷和響應(yīng)

private final static ObjectMapper objectMapper = new ObjectMapper();
...
// ajax請(qǐng)求
/**
 * 判斷是否已經(jīng)踢出
 * 1.如果是Ajax 訪問,那么給予json返回值提示。
 * 2.如果是普通請(qǐng)求,直接跳轉(zhuǎn)到登錄頁
 */
//判斷是不是Ajax請(qǐng)求
ResponseResult responseResult = new ResponseResult();
if (ShiroFilterUtils.isAjax(request) ) {
    logger.debug(getClass().getName()+ "當(dāng)前用戶已經(jīng)在其他地方登錄,并且是Ajax請(qǐng)求!");
    responseResult.setCode(IStatusMessage.SystemStatus.MANY_LOGINS.getCode());
    responseResult.setMessage("您已在別處登錄,請(qǐng)您修改密碼或重新登錄");
    out(response, responseResult);
}else{
    // 重定向
    WebUtils.issueRedirect(request, response, kickoutUrl);
}
...
/**
 * 
 * @描述:response輸出json
 * @創(chuàng)建人:wyait
 * @創(chuàng)建時(shí)間:2018年4月24日 下午5:14:22
 * @param response
 * @param result
 */
public static void out(ServletResponse response, ResponseResult result){
    PrintWriter out = null;
    try {
        response.setCharacterEncoding("UTF-8");//設(shè)置編碼
        response.setContentType("application/json");//設(shè)置返回類型
        out = response.getWriter();
        out.println(objectMapper.writeValueAsString(result));//輸出
        logger.error("用戶在線數(shù)量限制【wyait-manager-->KickoutSessionFilter.out】響應(yīng)json信息成功");
    } catch (Exception e) {
        logger.error("用戶在線數(shù)量限制【wyait-manager-->KickoutSessionFilter.out】響應(yīng)json信息出錯(cuò)", e);
    }finally{
        if(null != out){
            out.flush();
            out.close();
        }
    }
}
  1. 前端編寫公共判斷用戶是否登錄方法isLogin

/**
 * 判斷是否登錄,沒登錄刷新當(dāng)前頁,促使Shiro攔截后跳轉(zhuǎn)登錄頁
 * @param result    ajax請(qǐng)求返回的值
 * @returns {如果沒登錄,刷新當(dāng)前頁}
 */
function isLogin(result){
    if(result && result.code && result.code == '1101'){
        window.location.reload(true);//刷新當(dāng)前頁
    }
    return true;//返回true
}
  1. js中ajax調(diào)用isLogin方法

$.post("/user/delUser",{"id":id},function(data){
    //判斷用戶是否登錄
    if(isLogin(data)){
        if(data=="ok"){
            //回調(diào)彈框
            layer.alert("刪除成功!",function(){
                layer.closeAll();
                //加載load方法
                load(obj);//自定義
            });
        }else{
            layer.alert(data);//彈出錯(cuò)誤提示
        }
    }
});

只改動(dòng)了userList.js用戶列表界面,其他界面//TODO  

  1. 測(cè)試  
    同一個(gè)用戶在線沖突測(cè)試,然后點(diǎn)擊先登錄用戶界面中其中一個(gè)ajax方法,如果后臺(tái)用戶已退出,前臺(tái)isLogin刷新頁面,重新請(qǐng)求重定向到/toLogin?kickout頁面,最終跳轉(zhuǎn)到登錄界面。

session有效時(shí)間設(shè)置

session默認(rèn)有效時(shí)間:30分鐘(1800s)

  • spring boot session時(shí)間配置:

    # 會(huì)話超時(shí)(秒)1天
    server.session.timeout=86400
  • session有效時(shí)間問題  
    使用shiro進(jìn)行用戶在線數(shù)量限制功能中,securityManager配置sessionManager之后,springboot中配置的session有效時(shí)間無效(sessionManager管理器覆蓋了springboot中session有效時(shí)間的配置)。

session過期問題

使用shiro進(jìn)行用戶在線數(shù)量限制功能;用戶登錄后,2分鐘不操作,之后session失效。

原因

  1. spring boot整合shiro,在使用shiro進(jìn)行用戶在線數(shù)量限制時(shí),重新配置了SessionManger,

// //注入session管理器;
securityManager.setSessionManager(sessionManager());

SessionManager,配置EnterpriseCacheSessionDAO:

sessionManager.setSessionDAO(enterCacheSessionDAO());

EnterpriseCacheSessionDAO類,存取session的時(shí)候,是通過ehcache緩存中操作的。

這里如果配置有緩存的話需要給其配置一個(gè)cache的鍵類似于:

shiro默認(rèn)了一個(gè)默認(rèn)值為:shiro-activeSessionCache,如果不相同(cache文件中的鍵值) 需要進(jìn)行替換,最終進(jìn)行session存取的類為CachingSessionDAO

緩存管理器使用的是org.apache.shiro.cache.ehcache.EhCacheManager,那么最終shiro在找session的時(shí)候也會(huì)調(diào)用getCache。

Ehcache.xml配置

<!-- shiro-activeSessionCache活躍用戶session緩存策略 -->
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           timeToIdleSeconds="120"
           timeToLiveSeconds="120"
           maxElementsOnDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
    </cache>

這里配置了session緩存時(shí)間為2分鐘,故會(huì)出現(xiàn)登錄2分鐘無操作后,session失效問題。

  1. shiro拿到ehcache緩存中的session后,和服務(wù)器中的session校驗(yàn)匹配,這時(shí),如果服務(wù)器的session失效,也會(huì)出現(xiàn)問題;  
    假設(shè)設(shè)置服務(wù)器端當(dāng)前用戶的session為30s【

    SecurityUtils.getSubject().getSession().setTimeout(30000);//毫秒

    】,ehcache中session有效時(shí)間120s不變;在無操作30s后,請(qǐng)求后臺(tái),報(bào)錯(cuò)如下:

org.apache.shiro.session.ExpiredSessionException: Session with id [8aac0daf-c432-44b6-86cc-a618095ad2bd] has expired. Last access time: 18-4-24 上午11:32.  Current time: 18-4-24 上午11:33.  Session timeout is set to 30 seconds (0 minutes)
    at org.apache.shiro.session.mgt.SimpleSession.validate(SimpleSession.java:292) ~[shiro-core-1.3.1.jar:1.3.1]
    at org.apache.shiro.session.mgt.AbstractValidatingSessionManager.doValidate(AbstractValidatingSessionManager.java:186) ~[shiro-core-1.3.1.jar:1.3.1]
... ...

故ehcache緩存中session的有效時(shí)間和服務(wù)器端session有效時(shí)間必須配置一致。

解決方案

  1. 服務(wù)端session時(shí)間設(shè)置:

//session有效時(shí)間1天(毫秒)
SecurityUtils.getSubject().getSession().setTimeout(86400000);
  • 設(shè)置的最大時(shí)間,正負(fù)都可以,為負(fù)數(shù)時(shí)表示永不超時(shí)。

    SecurityUtils.getSubject().getSession().setTimeout(-1000l);

    注意:這里設(shè)置的時(shí)間單位是:ms,但是Shiro會(huì)把這個(gè)時(shí)間轉(zhuǎn)成:s,而且是會(huì)舍掉小數(shù)部分,這樣設(shè)置的是-1ms,轉(zhuǎn)成s后就是0s,馬上就過期了。所有要是除以1000以后還是負(fù)數(shù),必須設(shè)置小于-1000

  1. 將Ehcache.xml時(shí)間配置和服務(wù)器設(shè)置的session有效時(shí)間保持一致。

    <!-- shiro-activeSessionCache活躍用戶session緩存策略(秒) -->
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           timeToIdleSeconds="86400"
           timeToLiveSeconds="86400"
           maxElementsOnDisk="10000000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU">
    </cache>

    通過代碼中查看session有效時(shí)間:

logger.debug("session設(shè)置的有效時(shí)間:"+request.getSession().getMaxInactiveInterval());
logger.debug("shiro中session設(shè)置的有效時(shí)間:"+SecurityUtils.getSubject().getSession().getTimeout());
//86400(秒)
//86400000(毫秒)

總結(jié)

具體實(shí)現(xiàn)可以根據(jù)具體需求做調(diào)整;近期提供redis實(shí)現(xiàn)版本。

20180426版本更新內(nèi)容

  1. 編輯用戶自己成功后,執(zhí)行退出,重新登錄信息生效;

  2. 禁止用戶刪除自己;

  3. 優(yōu)化用戶列表操作信息提示;

  4. 角色管理列表,通過添加參數(shù)callback,實(shí)現(xiàn)菜單回顯選中;

20180503版本更新內(nèi)容

  1. 新增用戶表version版本字段;

  2. 更新用戶操作,通過version字段來保證數(shù)據(jù)一致;

  3. 新增通過攔截器實(shí)現(xiàn)動(dòng)態(tài)更新用戶信息(同步更新在線用戶信息);

  4. 新增登錄成功后默認(rèn)頁面home.html;

  5. 頁面操作細(xì)節(jié)優(yōu)化。

spring boot + shiro 動(dòng)態(tài)更新用戶信息

鏈接入口--> spring boot + shiro 動(dòng)態(tài)更新用戶信息:https://blog.51cto.com/wyait/2112200

20180606版本更新內(nèi)容

  1. 新增shiro權(quán)限注解;

  2. 請(qǐng)求亂碼問題解決;

  3. 統(tǒng)一異常處理;

  4. 頁面操作細(xì)節(jié)優(yōu)化。

springboot + shiro 權(quán)限注解、統(tǒng)一異常處理、請(qǐng)求亂碼解決

鏈接入口--> springboot + shiro 權(quán)限注解、統(tǒng)一異常處理、請(qǐng)求亂碼解決 :https://blog.51cto.com/wyait/2125708

TODO

  • 后臺(tái)方法級(jí)別權(quán)限控制,通過shiro配置可實(shí)現(xiàn);具體用戶管理的操作根據(jù)業(yè)務(wù)實(shí)際的需求可做調(diào)整;

關(guān)于“springboot+shiro之登錄人數(shù)限制、登錄判斷重定向、session時(shí)間設(shè)置的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI