This commit is contained in:
Ludy87 2025-03-28 21:53:17 +01:00
parent 762571f42b
commit fe4d2823aa
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
14 changed files with 214 additions and 200 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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<String, SessionsModelInterface> 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<SessionsModelInterface> 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<SessionsModelInterface> 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());
}
}

View File

@ -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<AnonymusSessionInfo> 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());
}
});
}
}

View File

@ -19,10 +19,11 @@ public class AnonymusSessionStatusController {
public ResponseEntity<String> 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<String> 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<String> expireAllSessions() {
// Invalidate all sessions
sessionRegistry.expireAllSessions();
return ResponseEntity.ok("All sessions invalidated");
}
@GetMapping("/session/expire/{username}")
public ResponseEntity<String> expireAllSessionsByUsername(@PathVariable String username) {
// Invalidate all sessions for specific user
sessionRegistry.expireAllSessionsByUsername(username);
return ResponseEntity.ok("All sessions invalidated for user: " + username);
}

View File

@ -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<SessionsModelInterface> getAllSessions();
Collection<SessionsModelInterface> getAllNonExpiredSessions();
Collection<SessionsModelInterface> getAllNonExpiredSessionsBySessionId(String sessionId);
void registerSession(HttpSession session);
void removeSession(HttpSession session);

View File

@ -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;
}
}

View File

@ -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<SessionsModelInterface> getAllNonExpiredSessionsBySessionId(
String sessionId) {
return sessionPersistentRegistry.getAllNonExpiredSessionsBySessionId(sessionId).stream()
.map(session -> (SessionsModelInterface) session)
.toList();
}
@Override
public Collection<SessionsModelInterface> getAllSessions() {
return new ArrayList<>(sessionPersistentRegistry.getAllSessions());
}
@Override
public boolean isSessionValid(String sessionId) {
List<SessionEntity> 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<SessionEntity> 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

View File

@ -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);
}

View File

@ -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<SessionEntity> getAllNonExpiredSessionsBySessionId(String sessionId) {
return sessionRepository.findBySessionIdAndExpired(sessionId, false);
}
// Retrieve all non-expired sessions
public List<SessionEntity> 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<SessionEntity> 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<SessionEntity> findLatestSession(String principalName) {

View File

@ -21,8 +21,6 @@ public interface SessionRepository extends JpaRepository<SessionEntity, String>
SessionEntity findBySessionId(String sessionId);
List<SessionEntity> findBySessionIdAndExpired(String sessionId, boolean expired);
void deleteByPrincipalName(String principalName);
@Modifying

View File

@ -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,

View File

@ -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<String> 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<String> 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<String> expireSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
@ -71,12 +89,15 @@ public class SessionStatusController {
}
}
// Invalidates all sessions
@GetMapping("/session/expire/all")
public ResponseEntity<String> 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<String> expireAllSessionsByUsername(@PathVariable String username) {
SecurityContext cxt = SecurityContextHolder.getContext();

View File

@ -3,8 +3,7 @@
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block
th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
</th:block>
<style>
.active-user {
@ -52,16 +51,22 @@
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
</a>
<a href="/usage" th:if="${@runningEE}" class="btn btn-outline-success"
th:title="#{adminUserSettings.usage}">
<a href="/usage" th:if="${@runningEE}" class="btn btn-outline-success" th:title="#{adminUserSettings.usage}">
<span class="material-symbols-rounded">analytics</span>
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
</a>
<div class="my-4">
<strong th:if="${@runningEE}" style="margin-left: 20px;"
text="#{adminUserSettings.totalUsers}">runningEE</strong>
<strong th:if="${!@runningEE}" style="margin-left: 20px;"
text="#{adminUserSettings.totalUsers}">Non-Paid</strong>
</div>
<div class="my-4">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
<span th:text="${totalUsers}"></span>
<span th:if="${@runningProOrHigher}" th:text="'/'+${maxPaidUsers}"></span>
<span th:if="${@runningProOrHigher}" th:text="'| ' + ${maxPaidUsers}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
<span th:text="${activeUsers}"></span>
@ -69,14 +74,12 @@
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
<span th:text="${disabledUsers}"></span>
<th:block th:if="${@runningProOrHigher}">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total
Sessions:</strong>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
<span th:text="${sessionCount}"></span>
</th:block>
<th:block th:if="${!@runningProOrHigher}">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total
Sessions:</strong>
<span th:text="${sessionCount}"></span>/<span th:text="${maxSessions}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
<span th:text="${sessionCount}"></span><span th:if="${!@runningEE}" th:text="' | ' + ${maxSessions}"></span>
</th:block>
</div>
</div>
@ -103,16 +106,15 @@
<tr>
<th scope="col">#</th>
<th scope="col" th:title="#{username}" th:text="#{username}">Username</th>
<th scope="col" th:title="#{adminUserSettings.roles}" th:text="#{adminUserSettings.roles}">Roles
</th>
<th scope="col" th:title="#{adminUserSettings.roles}" th:text="#{adminUserSettings.roles}">Roles</th>
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow"
th:text="#{adminUserSettings.authenticated}">Authenticated</th>
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow"
th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:title="#{adminUserSettings.userSessions}"
th:text="#{adminUserSettings.userSessions}">User Sessions</th>
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}"
colspan="2">Actions</th>
<th scope="col" th:title="#{adminUserSettings.userSessions}" th:text="#{adminUserSettings.userSessions}">
User Sessions</th>
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}" colspan="2">
Actions</th>
<!-- <th scope="col"></th> -->
</tr>
</thead>
@ -140,8 +142,7 @@
<form th:if="${user.username != currentUsername}"
th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post"
onsubmit="return confirmDeleteUser()">
<button type="submit" th:title="#{adminUserSettings.deleteUser}"
class="btn btn-info btn-sm"><span
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="btn btn-info btn-sm"><span
class="material-symbols-rounded">person_remove</span></button>
</form>
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}"
@ -202,16 +203,16 @@
<label for="username" th:text="#{username}">Username</label>
<select name="username" class="form-control" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="user : ${users}" th:if="${user.username != currentUsername}"
th:value="${user.username}" th:text="${user.username}">Username</option>
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}"
th:text="${user.username}">Username</option>
</select>
</div>
<div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}"
th:text="#{${roleDetail.value}}">Role</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">
Role</option>
</select>
</div>
@ -254,8 +255,8 @@
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" id="role" required>
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}"
th:text="#{${roleDetail.value}}">Role</option>
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">
Role</option>
</select>
</div>
<div class="mb-3">