【Shiro第七篇】SpringBoot + Shiro實現會話管理

人醜就該多讀書呀發表於2020-11-21

目錄

一、概述

二、重要概念

三、Shiro線上會話管理

四、總結


一、概述

Shiro 提供了完整的企業級會話管理功能,不依賴於底層容器(如 web 容器 tomcat),不管 JavaSE 還是 JavaEE 環境都可以使用,提供了會話管理、會話事件監聽、會話儲存 / 持久化、容器無關的叢集、失效 / 過期支援、對 Web 的透明支援、SSO 單點登入的支援等特性,即直接使用 Shiro 的會話管理可以直接替換如 Web 容器的會話管理。

二、重要概念

  • 會話

所謂會話,即使用者訪問應用時保持的連線關係,在多次互動中應用能夠識別出當前訪問的使用者是誰,且可以在多次互動中儲存一些資料。如訪問一些網站時登入成功後,網站可以記住使用者,且在退出之前都可以識別當前使用者是誰。

subject.getSession(); 獲取Session物件
session.getId(); 獲取當前會話的唯一標識。
session.getHost(); 獲取當前 Subject 的主機地址
session.getTimeout(); / session.setTimeout(毫秒); 獲取 / 設定當前 Session 的過期時間;如果不設定預設是會話管理器的全域性過期時間。
session.getStartTimestamp(); / session.getLastAccessTime(); 獲取會話的啟動時間及最後訪問時間;
session.setAttribute("key", "123"); / session.removeAttribute("key"); 設定 / 獲取 / 刪除會話屬性
session.touch(); / session.stop(); 更新會話最後訪問時間及銷燬會話
  • 會話管理器

會話管理器管理著應用中所有 Subject 的會話的建立、維護、刪除、失效、驗證等工作,是 Shiro 的核心元件。Shiro 提供了三個預設實現:

DefaultSessionManager:DefaultSecurityManager 使用的預設實現,用於 JavaSE 環境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的預設實現,用於 Web 環境,其直接使用 Servlet 容器的會話;
DefaultWebSessionManager:用於 Web 環境的實現,可以替代 ServletContainerSessionManager,自己維護著會話,直接廢棄了 Servlet 容器的會話管理。
  • 會話監聽器

會話監聽器用於監聽會話建立、過期及停止事件。

onStart: 會話建立時觸發
onExpiration: 會話過期時觸發
onStop: 退出/會話過期時觸發
  • 會話儲存 / 持久化

Shiro 提供 SessionDAO 用於會話的 CRUD,即 DAO(Data Access Object)模式實現:

//如DefaultSessionManager在建立完session後會呼叫該方法;如儲存到關聯式資料庫/檔案系統/NoSQL資料庫;即可以實現會話的持久化;返回會話ID;主要此處返回的ID.equals(session.getId());
Serializable create(Session session);
//根據會話ID獲取會話
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新會話;如更新會話最後訪問時間/停止會話/設定超時時間/設定移除屬性等會呼叫
void update(Session session) throws UnknownSessionException;
//刪除會話;當會話過期/會話停止(如使用者退出時)會呼叫
void delete(Session session);
//獲取當前所有活躍使用者,如果使用者量多此方法影響效能
Collection<Session> getActiveSessions();
  • 會話驗證

Shiro 提供了會話驗證排程器,用於定期的驗證會話是否已過期,如果過期將停止會話;出於效能考慮,一般情況下都是獲取會話時來驗證會話是否過期並停止會話的;但是如在 web 環境中,如果使用者不主動退出是不知道會話是否過期的,因此需要定期的檢測會話是否過期,Shiro 提供了會話驗證排程器 SessionValidationScheduler 來做這件事情。

  • 線上會話管理

有時候需要顯示當前線上人數、當前線上使用者,有時候可能需要強制某個使用者下線等,此時就需要獲取相應的線上使用者並進行一些操作。

下面通過一個Shiro線上會話管理統計當前系統線上人數,查詢線上使用者資訊、強制讓某個使用者下線等等。

三、Shiro線上會話管理

此案例使用RedisSessionDAO結合Redis快取實現Shiro線上會話管理。

【a】Shiro全域性配置類注入RedisSessionDAO

/**
 * 配置redis管理器
 */
@Bean
public RedisManager redisManager() {
    RedisManager redisManager = new RedisManager();
    //設定一小時超時,單位是秒
    redisManager.setExpire(3600);
    return redisManager;
}

/**
 * 註冊RedisSessionDAO
 */
@Bean
public SessionDAO sessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    return redisSessionDAO;
}

【b】註冊SessionManager會話管理器

/**
 * 註冊SessionManager會話管理器
 */
@Bean
public SessionManager sessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    List<SessionListener> listeners = new ArrayList<>();
    //需要新增自己實現的會話監聽器
    listeners.add(new CustomShiroSessionListener());
    //新增會話監聽器給sessionManager管理
    sessionManager.setSessionListeners(listeners);
    //新增SessionDAO給sessionManager管理
    sessionManager.setSessionDAO(sessionDAO());
    //設定全域性(專案)session超時單位 毫秒   -1為永不超時
    sessionManager.setGlobalSessionTimeout(360000);
    return sessionManager;
}

因為SessionManager會話管理器需要新增個會話監聽,所以我們還得自定義一個會話監聽器,通過實現SessionListener介面實現。

import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @version V1.0
 * @ClassName: com.wsh.springboot.springbootshiro.listener.CustomShiroSessionListener.java
 * @Description: 自定義會話監聽器
 * @author: weishihuai
 * @date: 2020/11/7 15:03
 */
public class CustomShiroSessionListener implements SessionListener {
    private static final Logger logger = LoggerFactory.getLogger(CustomShiroSessionListener.class);

    /**
     * 維護著個原子型別的Integer物件,用於統計線上Session的數量
     */
    private final AtomicInteger sessionCount = new AtomicInteger(0);

    @Override
    public void onStart(Session session) {
        sessionCount.getAndIncrement();
        logger.info("使用者登入人數增加一人" + sessionCount.get());
    }

    @Override
    public void onStop(Session session) {
        sessionCount.decrementAndGet();
        logger.info("使用者登入人數減少一人" + sessionCount.get());
    }

    @Override
    public void onExpiration(Session session) {
        sessionCount.decrementAndGet();
        logger.info("使用者登入過期一人" + sessionCount.get());
    }
}

【c】將會話管理器交給SecurityManager進行管理

//設定會話管理器
defaultWebSecurityManager.setSessionManager(sessionManager());

【d】建立一個實體類用於儲存使用者線上資訊

public class OnlineUser {
    // session id
    private String sessionId;
    // 使用者id
    private String userId;
    // 使用者名稱稱
    private String username;
    // 使用者主機地址
    private String host;
    // 使用者登入時系統IP
    private String systemHost;
    // 狀態
    private String status;
    // session建立時間
    private Date startTimestamp;
    // session最後訪問時間
    private Date lastAccessTime;
    // 超時時間
    private Long timeout;

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getSystemHost() {
        return systemHost;
    }

    public void setSystemHost(String systemHost) {
        this.systemHost = systemHost;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Date getStartTimestamp() {
        return startTimestamp;
    }

    public void setStartTimestamp(Date startTimestamp) {
        this.startTimestamp = startTimestamp;
    }

    public Date getLastAccessTime() {
        return lastAccessTime;
    }

    public void setLastAccessTime(Date lastAccessTime) {
        this.lastAccessTime = lastAccessTime;
    }

    public Long getTimeout() {
        return timeout;
    }

    public void setTimeout(Long timeout) {
        this.timeout = timeout;
    }
}

【e】建立OnlineUserService

public interface OnlineUserService {

    /**
     * 獲取所有線上使用者資訊
     */
    List<OnlineUser> getAllOnlineUserList();

    /**
     * 根據sessionId強制登出
     *
     * @param sessionId 會話ID
     * @return
     */
    boolean forceLogout(String sessionId);

}

【f】建立OnlineUserService的實現類建立OnlineUserServiceImpl

@Service
public class OnlineUserServiceImpl implements OnlineUserService {

    /**
     * 注入會話dao
     */
    @Autowired
    private SessionDAO sessionDAO;

    @Autowired
    private UserMapper userMapper;

    @Override
    public List<OnlineUser> getAllOnlineUserList() {
        List<OnlineUser> onlineUserList = new ArrayList<>();
        //獲取到當前所有有效的Session物件
        Collection<Session> activeSessions = sessionDAO.getActiveSessions();
        OnlineUser userOnline;
        //迴圈遍歷所有有效的Session
        for (Session session : activeSessions) {
            userOnline = new OnlineUser();
            User user;
            SimplePrincipalCollection principalCollection;
            if (session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
                continue;
            } else {
                principalCollection = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
                String username = (String) principalCollection.getPrimaryPrincipal();
                user = userMapper.findUserByName(username);
                userOnline.setUsername(user.getUsername());
                userOnline.setUserId(user.getId());
            }
            userOnline.setSessionId((String) session.getId());
            userOnline.setHost(session.getHost());
            userOnline.setStartTimestamp(session.getStartTimestamp());
            userOnline.setLastAccessTime(session.getLastAccessTime());
            Long timeout = session.getTimeout();
            userOnline.setStatus(timeout.equals(0L) ? "離線" : "線上");
            userOnline.setTimeout(timeout);
            onlineUserList.add(userOnline);
        }
        return onlineUserList;
    }

    @Override
    public boolean forceLogout(String sessionId) {
        Session session = sessionDAO.readSession(sessionId);
        //強制登出
        sessionDAO.delete(session);
        return true;
    }

}

【g】建立OnlineUserController對外暴露操作介面

@Controller
public class OnlineUserController {

    @Autowired
    private OnlineUserService onlineUserService;

    @RequestMapping("/onlineUserList")
    public ModelAndView list() {
        List<OnlineUser> onlineUserList = onlineUserService.getAllOnlineUserList();
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("onlineUserList");
        modelAndView.addObject("onlineUserList", onlineUserList);
        return modelAndView;
    }

    @RequestMapping("/forceLogout")
    @ResponseBody
    public Map<String, String> forceLogout(@RequestParam("sessionId") String sessionId) {
        Map<String, String> resultMap = new HashMap<>(16);
        try {
            boolean forceLogout = onlineUserService.forceLogout(sessionId);
            if (forceLogout) {
                resultMap.put("code", "1");
                resultMap.put("msg", "強制踢人成功!");
            }
        } catch (Exception e) {
            resultMap.put("code", "0");
            resultMap.put("msg", "強制踢人失敗!");
            e.printStackTrace();
        }
        return resultMap;
    }

}

【h】新建onlineUserList.html,用於展示所有線上使用者

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>線上使用者管理</title>
</head>
<body>
<h3>線上使用者數: <span th:text="${onlineUserList.size()}"></span></h3>
<table border="1px">
    <tr>
        <th>使用者id</th>
        <th>使用者名稱稱</th>
        <th>登入時間</th>
        <th>最後訪問時間</th>
        <th>主機</th>
        <th>狀態</th>
        <th>會話ID</th>
    </tr>
    <tr th:each="user : ${onlineUserList}">
        <th th:text="${user.userId}"></th>
        <th th:text="${user.username}"></th>
        <th th:text="${#dates.format(user.startTimestamp, 'yyyy-MM-dd HH:mm:ss')}"></th>
        <th th:text="${#dates.format(user.lastAccessTime, 'yyyy-MM-dd HH:mm:ss')}"></th>
        <th th:text="${user.host}"></th>
        <th th:text="${user.status}"></th>
        <th th:text="${user.sessionId}"></th>
    </tr>
</table>
</body>
</html>

【i】success.html加入如下超連結,用於跳轉檢視所有線上使用者列表

<div>跳轉到onlineUserList.html: <a href="/onlineUserList">檢視使用者線上管理列表</a><br></div>

【j】測試

啟動專案,分別使用兩個瀏覽器,一個瀏覽器用admin/123456登入,一個瀏覽器使用user/123456進行登入。

然後點選檢視線上使用者列表:

可以看到,當前線上使用者是兩個,這就完成了線上使用者管理功能。

接下來我們測試一下強制踢出某個使用者,使用會話ID進行踢出,這裡為了方便,使用postman方式,傳入會話ID,去刪除會話資訊:

然後再次檢視當前使用者列表:

可以看到,admin使用者已經被踢出了。

注意:由於使用外部直接呼叫介面的方式去踢出,所以在Shiro配置類中需要放行/forceLogout介面。

filterChainDefinitionMap.put("/forceLogout", "anon");

四、總結

本篇文章主要總結了Shiro結合Redis實現線上會話管理功能,並通過一個統計當前線上使用者總人數、強制踢出使用者的小案例,說明了相關API的使用方法,本文采用的是RedisSessionDAO,即用的redis快取。

Shiro也支援使用Ehcache快取實現,那麼在Shiro配置類中就需要注入MemorySessionDAO物件,而不是RedisSessionDAO。

@Bean
public SessionDAO sessionDAO() {
    MemorySessionDAO sessionDAO = new MemorySessionDAO();
    return sessionDAO;
}

說明:當某個使用者被踢出後(Session Time置為0),該Session並不會立刻從ActiveSessions中剔除,所以我們可以通過其timeout資訊來判斷該使用者線上與否。

相關文章