From fe4d2823aa2c1669bf39b89b09ec18f34127b2b9 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Fri, 28 Mar 2025 21:53:17 +0100 Subject: [PATCH] clean up --- .../SPDF/config/EndpointInterceptor.java | 56 ++++++++++----- .../anonymus/session/AnonymusSessionInfo.java | 13 ++-- .../session/AnonymusSessionRegistry.java | 45 +++--------- .../session/AnonymusSessionService.java | 38 +++++------ .../AnonymusSessionStatusController.java | 21 ++++-- .../config/interfaces/SessionsInterface.java | 6 -- .../security/UserAuthenticationFilter.java | 23 +++++-- .../session/CustomHttpSessionListener.java | 68 ++++++------------- .../session/PreLogoutDataCaptureHandler.java | 6 ++ .../session/SessionPersistentRegistry.java | 54 ++++++++------- .../security/session/SessionRepository.java | 2 - .../security/session/SessionScheduled.java | 6 +- .../session/SessionStatusController.java | 27 +++++++- .../resources/templates/adminSettings.html | 49 ++++++------- 14 files changed, 214 insertions(+), 200 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index b58c5a11..8b8d7521 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -2,7 +2,6 @@ package stirling.software.SPDF.config; import java.security.Principal; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -20,15 +19,11 @@ public class EndpointInterceptor implements HandlerInterceptor { private final EndpointConfiguration endpointConfiguration; private final SessionsInterface sessionsInterface; - private boolean loginEnabled = true; public EndpointInterceptor( - EndpointConfiguration endpointConfiguration, - SessionsInterface sessionsInterface, - @Qualifier("loginEnabled") boolean loginEnabled) { + EndpointConfiguration endpointConfiguration, SessionsInterface sessionsInterface) { this.endpointConfiguration = endpointConfiguration; this.sessionsInterface = sessionsInterface; - this.loginEnabled = loginEnabled; } @Override @@ -44,6 +39,7 @@ public class EndpointInterceptor implements HandlerInterceptor { Principal principal = request.getUserPrincipal(); + // allowlist for public or static routes if ("/".equals(request.getRequestURI()) || "/login".equals(request.getRequestURI()) || "/home".equals(request.getRequestURI()) @@ -70,7 +66,6 @@ public class EndpointInterceptor implements HandlerInterceptor { final String currentPrincipal = principal.getName(); - // Zähle alle nicht abgelaufenen Sessions des aktuellen Benutzers. long userSessions = sessionsInterface.getAllSessions().stream() .filter( @@ -80,25 +75,19 @@ public class EndpointInterceptor implements HandlerInterceptor { s.getPrincipalName())) .count(); - // Zähle alle nicht abgelaufenen Sessions in der Anwendung. long totalSessions = sessionsInterface.getAllSessions().stream() .filter(s -> !s.isExpired()) .count(); - log.info( - "Aktive Sessions für {}: {} (max: {}) | Gesamt: {} (max: {})", + log.debug( + "Active sessions for {}: {} (max: {}) | Total: {} (max: {})", currentPrincipal, userSessions, sessionsInterface.getMaxUserSessions(), totalSessions, sessionsInterface.getMaxApplicationSessions()); - // Prüfe die Grenzen: - // Falls entweder die Benutzersessions oder die Anwendungssessions das Limit - // erreicht haben - // und die aktuelle Session noch NICHT registriert ist, dann wird ein Fehler - // zurückgegeben. boolean isCurrentSessionRegistered = sessionsInterface.getAllSessions().stream() .filter(s -> !s.isExpired()) @@ -114,8 +103,40 @@ public class EndpointInterceptor implements HandlerInterceptor { return false; } - // Wenn die Session noch nicht registriert ist, registriere sie; andernfalls update - // den Last-Request. + // If session is not registered yet, register it; otherwise, update the last request + // timestamp. + if (!isCurrentSessionRegistered) { + log.info("Register session: {}", sessionId); + sessionsInterface.registerSession(finalSession); + } else { + log.info("Update session last request: {}", sessionId); + sessionsInterface.updateSessionLastRequest(sessionId); + } + return true; + } else if (principal == null) { + if (session == null) { + session = request.getSession(true); + } + final HttpSession finalSession = session; + String sessionId = finalSession.getId(); + + long totalSessions = + sessionsInterface.getAllSessions().stream() + .filter(s -> !s.isExpired()) + .count(); + boolean isCurrentSessionRegistered = + sessionsInterface.getAllSessions().stream() + .filter(s -> !s.isExpired()) + .anyMatch(s -> s.getSessionId().equals(sessionId)); + + if (totalSessions >= sessionsInterface.getMaxApplicationSessions() + && !isCurrentSessionRegistered) { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, + "Max sessions reached for this user. To continue on this device, please" + + " close your session in another browser."); + return false; + } if (!isCurrentSessionRegistered) { log.info("Register session: {}", sessionId); sessionsInterface.registerSession(finalSession); @@ -128,6 +149,7 @@ public class EndpointInterceptor implements HandlerInterceptor { } String requestURI = request.getRequestURI(); + // Check if endpoint is enabled in config if (!endpointConfiguration.isEndpointEnabled(requestURI)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); return false; diff --git a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionInfo.java b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionInfo.java index 5543deac..54beaa46 100644 --- a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionInfo.java +++ b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionInfo.java @@ -5,13 +5,16 @@ import java.util.Date; import jakarta.servlet.http.HttpSession; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Setter; import lombok.ToString; import stirling.software.SPDF.config.interfaces.SessionsModelInterface; @Setter -@ToString(exclude = "session") +@ToString(exclude = "session") // exclude session from toString to avoid verbose output or sensitive +// data +@AllArgsConstructor public class AnonymusSessionInfo implements SessionsModelInterface { private static final String principalName = "anonymousUser"; private HttpSession session; @@ -22,14 +25,6 @@ public class AnonymusSessionInfo implements SessionsModelInterface { private Date lastRequest; private Boolean expired; - public AnonymusSessionInfo( - HttpSession session, Date createdAt, Date lastRequest, Boolean expired) { - this.session = session; - this.createdAt = createdAt; - this.lastRequest = lastRequest; - this.expired = expired; - } - public HttpSession getSession() { return session; } diff --git a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionRegistry.java b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionRegistry.java index 5cbb967a..af21da93 100644 --- a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionRegistry.java @@ -4,7 +4,6 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collection; -import java.util.Comparator; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -28,7 +27,7 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt @Value("${server.servlet.session.timeout:120s}") // TODO: Change to 30m private Duration defaultMaxInactiveInterval; - // Map zur Speicherung der Sessions inkl. Timestamp + // Map for storing sessions including timestamp private static final Map sessions = new ConcurrentHashMap<>(); @Override @@ -42,7 +41,7 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt return; } - // Speichern des Erstellungszeitpunkts + // Save creation timestamp Date creationTime = new Date(); int allNonExpiredSessions = getAllNonExpiredSessions().size(); @@ -78,11 +77,11 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt if (now.isAfter(expirationTime)) { sessionsInfo.setExpired(true); session.invalidate(); - log.info("Session {} wurde Expired=TRUE", session.getId()); + log.debug("Session {} expired=TRUE", session.getId()); } } - // Make a session as expired + // Mark a single session as expired public void expireSession(String sessionId) { if (sessions.containsKey(sessionId)) { AnonymusSessionInfo sessionInfo = (AnonymusSessionInfo) sessions.get(sessionId); @@ -90,12 +89,12 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt try { sessionInfo.getSession().invalidate(); } catch (IllegalStateException e) { - log.info("Session {} ist bereits invalidiert", sessionInfo.getSession().getId()); + log.debug("Session {} already invalidated", sessionInfo.getSession().getId()); } } } - // Make all sessions as expired + // Mark all sessions as expired public void expireAllSessions() { sessions.values() .forEach( @@ -106,12 +105,12 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt try { session.invalidate(); } catch (IllegalStateException e) { - log.info("Session {} ist bereits invalidiert", session.getId()); + log.debug("Session {} already invalidated", session.getId()); } }); } - // Mark all sessions as expired by username + // Expire all sessions by username public void expireAllSessionsByUsername(String username) { sessions.values().stream() .filter( @@ -127,27 +126,11 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt try { session.invalidate(); } catch (IllegalStateException e) { - log.info("Session {} ist bereits invalidiert", session.getId()); + log.debug("Session {} already invalidated", session.getId()); } }); } - @Override - public boolean isSessionValid(String sessionId) { - boolean exists = sessions.containsKey(sessionId); - boolean expired = exists ? sessions.get(sessionId).isExpired() : false; - return exists && !expired; - } - - @Override - public boolean isOldestNonExpiredSession(String sessionId) { - Collection nonExpiredSessions = getAllNonExpiredSessions(); - return nonExpiredSessions.stream() - .min(Comparator.comparing(SessionsModelInterface::getLastRequest)) - .map(oldest -> oldest.getSessionId().equals(sessionId)) - .orElse(false); - } - @Override public void updateSessionLastRequest(String sessionId) { if (sessions.containsKey(sessionId)) { @@ -174,21 +157,13 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt sessions.clear(); } - @Override - public Collection getAllNonExpiredSessionsBySessionId( - String sessionId) { - return sessions.values().stream() - .filter(info -> !info.isExpired() && info.getSessionId().equals(sessionId)) - .toList(); - } - @Override public void registerSession(HttpSession session) { if (!sessions.containsKey(session.getId())) { AnonymusSessionInfo sessionInfo = new AnonymusSessionInfo(session, new Date(), new Date(), false); sessions.put(session.getId(), sessionInfo); - log.info("Session {} wurde registriert", session.getId()); + log.debug("Session {} registered", session.getId()); } } diff --git a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionService.java b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionService.java index cae44879..b0432eeb 100644 --- a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionService.java +++ b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionService.java @@ -4,16 +4,16 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; -import java.util.List; -import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Slf4j +@Component public class AnonymusSessionService { @Autowired private AnonymusSessionRegistry sessionRegistry; @@ -21,27 +21,25 @@ public class AnonymusSessionService { @Value("${server.servlet.session.timeout:120s}") // TODO: Change to 30m private Duration defaultMaxInactiveInterval; + // Runs every minute to expire inactive sessions @Scheduled(cron = "0 0/1 * * * ?") public void expireSessions() { Instant now = Instant.now(); - List allNonExpiredSessions = - sessionRegistry.getAllNonExpiredSessions().stream() - .map(s -> (AnonymusSessionInfo) s) - .collect(Collectors.toList()); - for (AnonymusSessionInfo sessionInformation : allNonExpiredSessions) { - Date lastRequest = sessionInformation.getLastRequest(); - int maxInactiveInterval = (int) defaultMaxInactiveInterval.getSeconds(); - Instant expirationTime = - lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS); + sessionRegistry.getAllSessions().stream() + .filter(session -> !session.isExpired()) + .forEach( + session -> { + Date lastRequest = session.getLastRequest(); + int maxInactiveInterval = (int) defaultMaxInactiveInterval.getSeconds(); + Instant expirationTime = + lastRequest + .toInstant() + .plus(maxInactiveInterval, ChronoUnit.SECONDS); - if (now.isAfter(expirationTime)) { - log.info( - "SessionID: {} expiration time: {} Current time: {}", - sessionInformation.getSession().getId(), - expirationTime, - now); - sessionInformation.setExpired(true); - } - } + if (now.isAfter(expirationTime)) { + log.debug("Session expiration triggered"); + sessionRegistry.expireSession(session.getSessionId()); + } + }); } } diff --git a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java index 5fbc3f78..4496ab06 100644 --- a/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java +++ b/src/main/java/stirling/software/SPDF/config/anonymus/session/AnonymusSessionStatusController.java @@ -19,10 +19,11 @@ public class AnonymusSessionStatusController { public ResponseEntity getSessionStatus(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { + // No session found return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found"); } - boolean isActivSesssion = + boolean isActiveSession = sessionRegistry.getAllSessions().stream() .filter(s -> s.getSessionId().equals(session.getId())) .anyMatch(s -> !s.isExpired()); @@ -33,12 +34,17 @@ public class AnonymusSessionStatusController { long userSessions = sessionCount; int maxUserSessions = sessionRegistry.getMaxUserSessions(); - if (userSessions >= maxUserSessions && !isActivSesssion) { + // Session invalid or expired + if (userSessions >= maxUserSessions && !isActiveSession) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Session ungültig oder abgelaufen"); - } else if (session.getId() != null && isActivSesssion) { - return ResponseEntity.ok("Session gültig: " + session.getId()); - } else { + .body("Session invalid or expired"); + } + // Valid session + else if (session.getId() != null && isActiveSession) { + return ResponseEntity.ok("Valid session: " + session.getId()); + } + // Fallback message with session count + else { return ResponseEntity.ok("User has " + userSessions + " sessions"); } } @@ -47,6 +53,7 @@ public class AnonymusSessionStatusController { public ResponseEntity expireSession(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session != null) { + // Invalidate current session sessionRegistry.expireSession(session.getId()); return ResponseEntity.ok("Session invalidated"); } else { @@ -56,12 +63,14 @@ public class AnonymusSessionStatusController { @GetMapping("/session/expire/all") public ResponseEntity expireAllSessions() { + // Invalidate all sessions sessionRegistry.expireAllSessions(); return ResponseEntity.ok("All sessions invalidated"); } @GetMapping("/session/expire/{username}") public ResponseEntity expireAllSessionsByUsername(@PathVariable String username) { + // Invalidate all sessions for specific user sessionRegistry.expireAllSessionsByUsername(username); return ResponseEntity.ok("All sessions invalidated for user: " + username); } diff --git a/src/main/java/stirling/software/SPDF/config/interfaces/SessionsInterface.java b/src/main/java/stirling/software/SPDF/config/interfaces/SessionsInterface.java index 60173fb6..d4d4314b 100644 --- a/src/main/java/stirling/software/SPDF/config/interfaces/SessionsInterface.java +++ b/src/main/java/stirling/software/SPDF/config/interfaces/SessionsInterface.java @@ -6,18 +6,12 @@ import jakarta.servlet.http.HttpSession; public interface SessionsInterface { - boolean isSessionValid(String sessionId); - - boolean isOldestNonExpiredSession(String sessionId); - void updateSessionLastRequest(String sessionId); Collection getAllSessions(); Collection getAllNonExpiredSessions(); - Collection getAllNonExpiredSessionsBySessionId(String sessionId); - void registerSession(HttpSession session); void removeSession(HttpSession session); diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index b2e9d184..b0684d75 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -58,13 +58,26 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!loginEnabledValue) { // If login is not enabled, just pass all requests without authentication filterChain.doFilter(request, response); return; } String requestURI = request.getRequestURI(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // Check for session expiration (unsure if needed) + // if (authentication != null && authentication.isAuthenticated()) { + // String sessionId = request.getSession().getId(); + // SessionInformation sessionInfo = + // sessionPersistentRegistry.getSessionInformation(sessionId); + // + // if (sessionInfo != null && sessionInfo.isExpired()) { + // SecurityContextHolder.clearContext(); + // response.sendRedirect(request.getContextPath() + "/login?expired=true"); + // return; + // } + // } // Check for API key in the request headers if no authentication exists if (authentication == null || !authentication.isAuthenticated()) { @@ -110,10 +123,10 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { response.getWriter() .write( "Authentication required. Please provide a X-API-KEY in request" - + " header.\n" - + "This is found in Settings -> Account Settings -> API Key\n" - + "Alternatively you can disable authentication if this is" - + " unexpected"); + + " header.\n" + + "This is found in Settings -> Account Settings -> API Key\n" + + "Alternatively you can disable authentication if this is" + + " unexpected"); return; } } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java index f8ed023c..00183d20 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java @@ -27,7 +27,6 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.interfaces.SessionsInterface; import stirling.software.SPDF.config.interfaces.SessionsModelInterface; import stirling.software.SPDF.config.security.UserUtils; -import stirling.software.SPDF.model.SessionEntity; @Component @Slf4j @@ -57,40 +56,11 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI .toList(); } - @Override - public Collection getAllNonExpiredSessionsBySessionId( - String sessionId) { - return sessionPersistentRegistry.getAllNonExpiredSessionsBySessionId(sessionId).stream() - .map(session -> (SessionsModelInterface) session) - .toList(); - } - @Override public Collection getAllSessions() { return new ArrayList<>(sessionPersistentRegistry.getAllSessions()); } - @Override - public boolean isSessionValid(String sessionId) { - List allSessions = sessionPersistentRegistry.getAllSessions(); - // gib zurück ob ist expired - return allSessions.stream() - .anyMatch( - session -> - session.getSessionId().equals(sessionId) && !session.isExpired()); - } - - @Override - public boolean isOldestNonExpiredSession(String sessionId) { - log.info("isOldestNonExpiredSession for sessionId: {}", sessionId); - List nonExpiredSessions = - sessionPersistentRegistry.getAllSessionsNotExpired(); - return nonExpiredSessions.stream() - .min(Comparator.comparing(SessionEntity::getLastRequest)) - .map(oldest -> oldest.getSessionId().equals(sessionId)) - .orElse(false); - } - @Override public void updateSessionLastRequest(String sessionId) { sessionPersistentRegistry.refreshLastRequest(sessionId); @@ -121,16 +91,20 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI if ("anonymousUser".equals(principalName) && loginEnabled) { return; } - int allNonExpiredSessions = getAllNonExpiredSessions().size(); - allNonExpiredSessions = - getAllSessions().stream() - .filter(s -> !s.isExpired()) - .filter(s -> s.getPrincipalName().equals(principalName)) - .filter(s -> "anonymousUser".equals(principalName) && !loginEnabled) - .peek(s -> log.info("Session {}", s.getPrincipalName())) - .toList() - .size(); + int allNonExpiredSessions; + + if ("anonymousUser".equals(principalName) && !loginEnabled) { + allNonExpiredSessions = + (int) getAllSessions().stream().filter(s -> !s.isExpired()).count(); + } else { + allNonExpiredSessions = + (int) + getAllSessions().stream() + .filter(s -> !s.isExpired()) + .filter(s -> s.getPrincipalName().equals(principalName)) + .count(); + } int all = getAllSessions().stream() @@ -138,7 +112,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI .toList() .size(); boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled; - log.info( + log.debug( "all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}", all, allNonExpiredSessions, @@ -146,7 +120,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI isAnonymousUserWithoutLogin); if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) { - log.info("Session {} Expired=TRUE", session.getId()); + log.debug("Session {} Expired=TRUE", session.getId()); sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); // if (allNonExpiredSessions > getMaxUserSessions()) { @@ -154,12 +128,12 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI // } } else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) { enforceMaxSessionsForPrincipal(principalName); - log.info("Session {} Expired=TRUE", principalName); + log.debug("Session {} Expired=TRUE", session.getId()); } else if (isAnonymousUserWithoutLogin) { sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); } else { - log.info("Session created: {}", principalName); + log.debug("Session created: {}", session.getId()); sessionPersistentRegistry.registerNewSession(se.getSession().getId(), principalName); } } @@ -175,7 +149,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI int maxAllowed = getMaxUserSessions(); if (userSessions.size() > maxAllowed) { int sessionsToRemove = userSessions.size() - maxAllowed; - log.info( + log.debug( "User {} has {} active sessions, removing {} oldest session(s).", principalName, userSessions.size(), @@ -186,7 +160,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI // die die Session anhand der Session-ID invalidieren und entfernen. sessionPersistentRegistry.expireSession(sessionModel.getSessionId()); sessionPersistentRegistry.removeSessionInformation(sessionModel.getSessionId()); - log.info( + log.debug( "Removed session {} for principal {}", sessionModel.getSessionId(), principalName); @@ -216,7 +190,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI sessionPersistentRegistry.expireSession(session.getId()); session.invalidate(); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); - log.info("Session {} wurde Expired=TRUE", session.getId()); + log.debug("Session {} expired=TRUE", session.getId()); } } @@ -230,7 +204,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI sessionPersistentRegistry.expireSession(session.getId()); session.invalidate(); sessionPersistentRegistry.removeSessionInformation(session.getId()); - log.info("Session {} wurde Expired=TRUE", session.getId()); + log.debug("Session {} expired=TRUE", session.getId()); } // Get the maximum number of sessions diff --git a/src/main/java/stirling/software/SPDF/config/security/session/PreLogoutDataCaptureHandler.java b/src/main/java/stirling/software/SPDF/config/security/session/PreLogoutDataCaptureHandler.java index af796c7f..7821927d 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/PreLogoutDataCaptureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/PreLogoutDataCaptureHandler.java @@ -31,14 +31,20 @@ public class PreLogoutDataCaptureHandler implements LogoutHandler { if (sessionId == null) { return; } + String path = request.getServletPath(); if (path == null) { return; } + + // Only handle explicit logout requests if (!"/logout".equals(path)) { return; } + log.debug("Session ID: {} Principal: {}", sessionId, authentication.getPrincipal()); + + // Mark the session as expired and remove its record sessionPersistentRegistry.expireSession(sessionId); sessionPersistentRegistry.removeSessionInformation(sessionId); } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index 72ea5364..40eca54a 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -76,11 +76,17 @@ public class SessionPersistentRegistry implements SessionRegistry { String principalName = UserUtils.getUsernameFromPrincipal(principal); if (principalName != null) { + + int sessionUserCount = getAllSessions(principalName, false).size(); + + if (sessionUserCount >= getMaxUserSessions()) { + return; + } SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId); if (sessionEntity == null) { sessionEntity = new SessionEntity(); sessionEntity.setSessionId(sessionId); - log.info("Registering new session for principal: {}", principalName); + log.debug("Registering new session for principal: {}", principalName); } sessionEntity.setPrincipalName(principalName); sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date @@ -128,7 +134,7 @@ public class SessionPersistentRegistry implements SessionRegistry { .collect(Collectors.toList()); if (nonExpiredSessions.isEmpty()) { - log.info("Keine nicht abgelaufenen Sessions für principal {} gefunden", principalName); + log.debug("No active sessions found for principal {}", principalName); return; } @@ -141,8 +147,8 @@ public class SessionPersistentRegistry implements SessionRegistry { SessionEntity oldestSession = oldestSessionOpt.get(); expireSession(oldestSession.getSessionId()); removeSessionInformation(oldestSession.getSessionId()); - log.info( - "Die älteste Session {} für principal {} wurde als expired markiert", + log.debug( + "Oldest session {} for principal {} has been marked as expired", oldestSession.getSessionId(), principalName); } @@ -161,11 +167,6 @@ public class SessionPersistentRegistry implements SessionRegistry { return null; } - // Retrieve all non-expired sessions - public List getAllNonExpiredSessionsBySessionId(String sessionId) { - return sessionRepository.findBySessionIdAndExpired(sessionId, false); - } - // Retrieve all non-expired sessions public List getAllSessionsNotExpired() { return sessionRepository.findByExpired(false); @@ -183,6 +184,7 @@ public class SessionPersistentRegistry implements SessionRegistry { SessionEntity sessionEntity = sessionEntityOpt.get(); sessionEntity.setExpired(true); // Set expired to true sessionRepository.save(sessionEntity); + log.debug("Session expired: {}", sessionId); } } @@ -192,6 +194,7 @@ public class SessionPersistentRegistry implements SessionRegistry { for (SessionEntity sessionEntity : sessionEntities) { sessionEntity.setExpired(true); // Set expired to true sessionRepository.save(sessionEntity); + log.debug("Session expired: {}", sessionEntity.getSessionId()); } } @@ -201,28 +204,29 @@ public class SessionPersistentRegistry implements SessionRegistry { for (SessionEntity sessionEntity : sessionEntities) { sessionEntity.setExpired(true); // Set expired to true sessionRepository.save(sessionEntity); + log.debug("Session expired: {}", sessionEntity.getSessionId()); } } // Mark all sessions as expired for a given principal name public void expireAllSessionsByPrincipalName(String principalName) { List sessionEntities = sessionRepository.findByPrincipalName(principalName); - log.info("Session entities: {}", sessionEntities.size()); + log.debug("Session entities: {}", sessionEntities.size()); for (SessionEntity sessionEntity : sessionEntities) { - log.info( + log.debug( "Session expired: {} {} {}", sessionEntity.getPrincipalName(), sessionEntity.isExpired(), sessionEntity.getSessionId()); sessionEntity.setExpired(true); // Set expired to true removeSessionInformation(sessionEntity.getSessionId()); - // sessionRepository.flush(); } + sessionEntities = sessionRepository.findByPrincipalName(principalName); - log.info("Session entities: {}", sessionEntities.size()); + log.debug("Session entities: {}", sessionEntities.size()); for (SessionEntity sessionEntity : sessionEntities) { if (sessionEntity.getPrincipalName().equals(principalName)) { - log.info("Session expired: {}", sessionEntity.getSessionId()); + log.debug("Session expired: {}", sessionEntity.getSessionId()); } } } @@ -238,19 +242,19 @@ public class SessionPersistentRegistry implements SessionRegistry { } // Update session details by principal name - public void updateSessionByPrincipalName( - String principalName, boolean expired, Date lastRequest) { - sessionRepository.saveByPrincipalName(expired, lastRequest, principalName); - } + // public void updateSessionByPrincipalName( + // String principalName, boolean expired, Date lastRequest) { + // sessionRepository.saveByPrincipalName(expired, lastRequest, principalName); + // } // Update session details by session ID - public void updateSessionBySessionId(String sessionId) { - SessionEntity sessionEntity = getSessionEntity(sessionId); - if (sessionEntity != null) { - sessionEntity.setLastRequest(new Date()); - sessionRepository.save(sessionEntity); - } - } + // public void updateSessionBySessionId(String sessionId) { + // SessionEntity sessionEntity = getSessionEntity(sessionId); + // if (sessionEntity != null) { + // sessionEntity.setLastRequest(new Date()); + // sessionRepository.save(sessionEntity); + // } + // } // Find the latest session for a given principal name public Optional findLatestSession(String principalName) { diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java index 0c4109a7..160a0375 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionRepository.java @@ -21,8 +21,6 @@ public interface SessionRepository extends JpaRepository SessionEntity findBySessionId(String sessionId); - List findBySessionIdAndExpired(String sessionId, boolean expired); - void deleteByPrincipalName(String principalName); @Modifying diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java index 25ba4532..dec1e668 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java @@ -36,7 +36,7 @@ public class SessionScheduled { if (principal == null) { continue; } else if (principal instanceof String stringPrincipal) { - // Skip anonymousUser if login is enabled + // Expire anonymousUser sessions if login is enabled if ("anonymousUser".equals(stringPrincipal) && loginEnabledValue) { sessionPersistentRegistry.expireAllSessionsByPrincipalName(stringPrincipal); continue; @@ -52,10 +52,14 @@ public class SessionScheduled { if (now.isAfter(expirationTime)) { sessionPersistentRegistry.expireSession(sessionInformation.getSessionId()); sessionInformation.expireNow(); + + // Invalidate current authentication if expired session belongs to current user if (authentication != null && principal.equals(authentication.getPrincipal())) { authentication.setAuthenticated(false); } + SecurityContextHolder.clearContext(); + log.debug( "Session expired for principal: {} SessionID: {}", principal, diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java index 26109e99..1e928a6f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionStatusController.java @@ -15,15 +15,30 @@ import org.springframework.web.bind.annotation.RestController; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; + import lombok.extern.slf4j.Slf4j; + import stirling.software.SPDF.config.security.UserUtils; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; @RestController +@Slf4j public class SessionStatusController { @Autowired private SessionPersistentRegistry sessionPersistentRegistry; + // Returns the current session ID or 401 if no session exists + @GetMapping("/session") + public ResponseEntity getSession(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found"); + } else { + return ResponseEntity.ok(session.getId()); + } + } + + // Checks if the session is active and valid according to user session limits @GetMapping("/session/status") public ResponseEntity getSessionStatus(HttpServletRequest request) { HttpSession session = request.getSession(false); @@ -46,20 +61,23 @@ public class SessionStatusController { int userSessions = allSessions.size(); int maxUserSessions = sessionPersistentRegistry.getMaxUserSessions(); + // Check if the current session is valid or expired based on the session registry if (userSessions >= maxUserSessions && !isActivSession) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("Session ungültig oder abgelaufen"); + .body("Session invalid or expired"); } else if (session.getId() != null && isActivSession) { - return ResponseEntity.ok("Session gültig: " + session.getId()); + return ResponseEntity.ok("Valid session: " + session.getId()); } else { return ResponseEntity.ok( "User: " + username + " has " + userSessions + " sessions"); } } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Session ungültig oder abgelaufen"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body("Session invalid or expired"); } } + // Invalidates the current session @GetMapping("/session/expire") public ResponseEntity expireSession(HttpServletRequest request) { HttpSession session = request.getSession(false); @@ -71,12 +89,15 @@ public class SessionStatusController { } } + // Invalidates all sessions @GetMapping("/session/expire/all") public ResponseEntity expireAllSessions() { + log.debug("Expire all sessions"); sessionPersistentRegistry.expireAllSessions(); return ResponseEntity.ok("All sessions invalidated"); } + // Invalidates all sessions for a specific user, only if requested by the same user @GetMapping("/session/expire/{username}") public ResponseEntity expireAllSessionsByUsername(@PathVariable String username) { SecurityContext cxt = SecurityContextHolder.getContext(); diff --git a/src/main/resources/templates/adminSettings.html b/src/main/resources/templates/adminSettings.html index 5ffecd57..9942e946 100644 --- a/src/main/resources/templates/adminSettings.html +++ b/src/main/resources/templates/adminSettings.html @@ -3,8 +3,7 @@ xmlns:th="https://www.thymeleaf.org"> - +