Spring Boot(2.6.7)应用中追踪用户登录登出的最佳方式咨询
Spring Boot追踪用户登录/登出行为的最佳方案
针对你的场景(无登录/登出页面、部分调用方不复用会话、会话过期异常),推荐以下几种落地性强的方案:
一、利用Spring Security认证事件监听(优先推荐)
如果你的应用基于Spring Security实现认证,直接监听认证相关事件是最精准的方式:
登录追踪:监听
AuthenticationSuccessEvent
每当用户认证成功(不管是通过接口、Token还是其他方式),这个事件都会触发,直接在事件处理器里记录登录行为即可,无需依赖会话创建。示例代码:
@Component public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> { private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onApplicationEvent(AuthenticationSuccessEvent event) { Authentication auth = event.getAuthentication(); String username = auth.getName(); // 记录登录日志:用户名、登录时间、客户端信息等 logger.info("用户 {} 登录成功", username); } }登出追踪:结合
LogoutSuccessEvent和SessionDestroyedEvent- 主动登出:如果有主动触发登出的接口,调用Spring Security的
LogoutHandler会触发LogoutSuccessEvent,监听此事件记录主动登出。 - 被动登出(会话过期/销毁):监听
SessionDestroyedEvent,从事件中获取用户信息,再通过SessionRegistry检查该用户是否还有其他活跃会话,没有则记录登出。
示例代码:
@Component public class SessionDestroyedListener implements ApplicationListener<SessionDestroyedEvent> { @Autowired private SessionRegistry sessionRegistry; private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onApplicationEvent(SessionDestroyedEvent event) { List<SecurityContext> contexts = event.getSecurityContexts(); for (SecurityContext context : contexts) { Authentication auth = context.getAuthentication(); if (auth != null && auth.isAuthenticated()) { String username = auth.getName(); // 检查用户是否还有其他活跃会话 for (Object principal : sessionRegistry.getAllPrincipals()) { if (principal instanceof UserDetails && ((UserDetails) principal).getUsername().equals(username)) { List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false); // 除了当前销毁的会话,还有其他活跃会话则不记录登出 if (sessions.size() <= 1) { logger.info("用户 {} 所有会话已销毁,触发被动登出", username); } break; } } } } } }- 主动登出:如果有主动触发登出的接口,调用Spring Security的
二、自定义会话监听器(无Spring Security场景)
如果你的应用未使用Spring Security,可直接实现HttpSessionListener来追踪会话关联的用户行为:
- 登录时机:在用户完成身份校验后,将用户信息存入
HttpSession,此时sessionCreated事件触发时,可判断会话是否关联了有效用户,从而记录登录。 - 登出时机:
sessionDestroyed事件触发时,读取会话中的用户信息,再通过全局会话存储(比如自定义的Map或Redis)检查该用户是否还有其他活跃会话,无则记录登出。
示例代码:
@Component public class CustomSessionListener implements HttpSessionListener { // 用ConcurrentHashMap存储用户活跃会话数,生产环境建议用Redis private static final Map<String, Integer> USER_SESSION_COUNT = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public void sessionCreated(HttpSessionEvent se) { HttpSession session = se.getSession(); UserInfo user = (UserInfo) session.getAttribute("currentUser"); if (user != null) { USER_SESSION_COUNT.put(user.getUsername(), USER_SESSION_COUNT.getOrDefault(user.getUsername(), 0) + 1); logger.info("用户 {} 创建新会话,当前活跃会话数:{}", user.getUsername(), USER_SESSION_COUNT.get(user.getUsername())); } } @Override public void sessionDestroyed(HttpSessionEvent se) { HttpSession session = se.getSession(); UserInfo user = (UserInfo) session.getAttribute("currentUser"); if (user != null) { int count = USER_SESSION_COUNT.getOrDefault(user.getUsername(), 0) - 1; if (count <= 0) { USER_SESSION_COUNT.remove(user.getUsername()); logger.info("用户 {} 所有会话已销毁,触发登出", user.getUsername()); } else { USER_SESSION_COUNT.put(user.getUsername(), count); logger.info("用户 {} 销毁一个会话,当前活跃会话数:{}", user.getUsername(), count); } } } }
三、结合SessionRegistry与定时任务(解决会话过期异常场景)
针对你当前会话无法自动过期的问题,可通过定时任务主动扫描SessionRegistry中的会话,判断是否过期并触发登出记录:
- 配置定时任务,定期检查所有会话的最后访问时间
- 对超过超时时间的会话,手动标记为过期并移除
- 检查用户剩余活跃会话,无则记录登出
示例代码:
@Component public class SessionTimeoutChecker { @Autowired private SessionRegistry sessionRegistry; // 从配置文件读取会话超时时间,单位秒 @Value("${server.servlet.session.timeout:1800}") private int sessionTimeout; private final Logger logger = LoggerFactory.getLogger(getClass()); @Scheduled(fixedRate = 60000) // 每分钟扫描一次 public void checkExpiredSessions() { List<Object> principals = sessionRegistry.getAllPrincipals(); for (Object principal : principals) { List<SessionInformation> sessions = sessionRegistry.getAllSessions(principal, false); for (SessionInformation sessionInfo : sessions) { long lastAccessedTime = sessionInfo.getLastRequest().getTime(); long currentTime = System.currentTimeMillis(); if (currentTime - lastAccessedTime > sessionTimeout * 1000) { sessionInfo.expireNow(); sessionRegistry.removeSessionInformation(sessionInfo.getSessionId()); // 检查用户是否还有其他活跃会话 List<SessionInformation> remainingSessions = sessionRegistry.getAllSessions(principal, false); if (remainingSessions.isEmpty()) { String username = ((UserDetails) principal).getUsername(); logger.info("用户 {} 会话已过期,所有会话销毁,触发登出", username); } } } } } }
前置注意事项
无论采用哪种方案,先解决会话无法过期的问题是核心前提:
- 检查
application.properties中server.servlet.session.timeout配置是否生效(注意单位:默认秒,若设置为30m则是30分钟) - 确认Jetty的会话配置未覆盖Spring Boot的设置,比如是否自定义了
SessionManager导致过期逻辑失效 - 若使用分布式会话(如Redis),检查会话存储的过期时间配置是否正确
内容的提问来源于stack exchange,提问作者doc
相关产品推荐
相关产品推荐

