【Shiro第七篇】SpringBoot + 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資訊來判斷該使用者線上與否。
相關文章
- 【Shiro】4.Springboot整合ShiroSpring Boot
- springboot整合shiro實現身份認證Spring Boot
- SpringBoot 整合 Shiro 實現登入攔截Spring Boot
- springboot-shiroSpring Boot
- SpringBoot與Shiro整合-許可權管理Spring Boot
- Shiro實現Basic認證
- springboot+shiro整合Spring Boot
- springboot Shiro 配置類Spring Boot
- Shiro許可權管理框架(一):Shiro的基本使用框架
- SpringBoot_4_integrate_ShiroSpring Boot
- SpringBoot極簡整合ShiroSpring Boot
- springboot 整合 Shiro 配置類Spring Boot
- 教你 Shiro + SpringBoot 整合 JWTSpring BootJWT
- Shiro許可權管理框架(二):Shiro結合Redis實現分散式環境下的Session共享框架Redis分散式Session
- springboot + shiro 實現登入認證和許可權控制Spring Boot
- Shiro實現使用者授權
- Spring Boot 整合 Shiro實現認證及授權管理Spring Boot
- shiro教程(2): shiro介紹
- 教你Shiro+SpringBoot整合JWTSpring BootJWT
- SpringBoot、MyBatis、Shiro、Thymeleaf整合思路Spring BootMyBatis
- SpringBoot+Shiro學習(七):Filter過濾器管理Spring BootFilter過濾器
- [翻譯-Shiro]-Apache Shiro 簡介Apache
- [翻譯-Shiro]-Apache Shiro 框架解析Apache框架
- 學習springBoot(11)shiro安全框架Spring Boot框架
- Shiro 教程
- Shiro【授權、整合Spirng、Shiro過濾器】過濾器
- Shiro系列教程之一Shiro簡介
- [翻譯-Shiro]-Apache Shiro Java認證指南ApacheJava
- [翻譯-Shiro]-Apache Shiro Java 授權指南ApacheJava
- [翻譯-Shiro]-Apache Shiro Java註解列表ApacheJava
- SpringBoot中Shiro快取使用Redis、EhcacheSpring Boot快取Redis
- springboot-許可權控制shiro(二)Spring Boot
- springboot+shiro 整合與基本應用Spring Boot
- springboot(十四):springboot整合shiro-登入認證和許可權管理Spring Boot
- shiro使用自定義session管理器Session
- Apache Shiro 快速入門教程,shiro 基礎教程Apache
- [翻譯-Shiro]-10分鐘教會你Apache ShiroApache
- Shiro+Redis實現tomcat叢集session共享RedisTomcatSession