clean up and more

This commit is contained in:
Ludy87 2025-04-14 18:58:34 +02:00
parent e76e83c427
commit 4ed49d0c15
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
9 changed files with 89 additions and 177 deletions

View File

@ -151,10 +151,10 @@ public class EndpointInterceptor implements HandlerInterceptor {
if ((userSessions >= maxUserSessions if ((userSessions >= maxUserSessions
|| totalSessionsNonExpired >= maxApplicationSessions) || totalSessionsNonExpired >= maxApplicationSessions)
&& !hasUserActiveSession) { && !hasUserActiveSession) {
response.sendError( log.info(
HttpServletResponse.SC_UNAUTHORIZED,
"Max sessions reached for this user. To continue on this device, please" "Max sessions reached for this user. To continue on this device, please"
+ " close your session in another browser."); + " close your session in another browser.");
response.sendError(HttpServletResponse.SC_EXPECTATION_FAILED);
return false; return false;
} }
@ -203,10 +203,10 @@ public class EndpointInterceptor implements HandlerInterceptor {
if (totalSessions >= maxApplicationSessions && !hasUserActiveSession) { if (totalSessions >= maxApplicationSessions && !hasUserActiveSession) {
sessionsInterface.removeSession(finalSession); sessionsInterface.removeSession(finalSession);
response.sendError( log.info(
HttpServletResponse.SC_UNAUTHORIZED,
"Max sessions reached for this user. To continue on this device, please" "Max sessions reached for this user. To continue on this device, please"
+ " close your session in another browser."); + " close your session in another browser.");
response.sendError(HttpServletResponse.SC_EXPECTATION_FAILED);
return false; return false;
} }
if (!hasUserActiveSession) { if (!hasUserActiveSession) {

View File

@ -193,8 +193,15 @@ public class AnonymusSessionListener implements HttpSessionListener, SessionsInt
@Override @Override
public void removeSession(HttpSession session) { public void removeSession(HttpSession session) {
AnonymusSessionInfo sessionsInfo = (AnonymusSessionInfo) sessions.get(session.getId()); AnonymusSessionInfo sessionsInfo = (AnonymusSessionInfo) sessions.get(session.getId());
sessionsInfo.setExpired(true); if (sessionsInfo != null) {
session.invalidate(); sessionsInfo.setExpired(true);
}
try {
session.invalidate();
} catch (IllegalStateException e) {
log.debug("Session {} already invalidated", session.getId());
}
sessions.remove(session.getId());
sessions.remove(session.getId()); sessions.remove(session.getId());
} }

View File

@ -1,9 +1,10 @@
package stirling.software.SPDF.config.anonymus.session; package stirling.software.SPDF.config.anonymus.session;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -12,72 +13,44 @@ import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
@Controller @Controller
@Slf4j @Slf4j
public class AnonymusSessionStatusController { public class AnonymusSessionStatusController {
@Autowired private AnonymusSessionListener sessionRegistry; @Autowired private AnonymusSessionListener sessionRegistry;
@GetMapping("/session/status") @GetMapping("/userSession")
public ResponseEntity<String> getSessionStatus(HttpServletRequest request) { public String getUserSessions(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session == null) {
// No session found
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found");
}
boolean isActiveSession =
sessionRegistry.getAllSessions().stream()
.filter(s -> s.getSessionId().equals(session.getId()))
.anyMatch(s -> !s.isExpired());
long sessionCount =
sessionRegistry.getAllSessions().stream().filter(s -> !s.isExpired()).count();
long userSessions = sessionCount;
int maxUserSessions = sessionRegistry.getMaxUserSessions();
// Session invalid or expired
if (userSessions >= maxUserSessions && !isActiveSession) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.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");
}
}
@GetMapping("/session/expire")
public String expireSession(HttpServletRequest request) {
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
if (session != null) { if (session != null) {
// Invalidate current session
sessionRegistry.expireFirstSession(session.getId()); boolean isSessionValid =
log.info("Session invalidated: {}", session.getId()); sessionRegistry.getAllNonExpiredSessions().stream()
// return ResponseEntity.ok("Session invalidated"); .allMatch(
} else { sessionEntity ->
log.info("No session to invalidate"); sessionEntity.getSessionId().equals(session.getId()));
// return ResponseEntity.ok("No session to invalidate");
// Get all sessions for the user
List<SessionsModelInterface> sessionList =
sessionRegistry.getAllNonExpiredSessions().stream()
.filter(
sessionEntity ->
!sessionEntity.getSessionId().equals(session.getId()))
.toList();
model.addAttribute("sessionList", sessionList);
return "userSession";
} }
return "redirect:/"; return "redirect:/";
} }
@GetMapping("/session/expire/all") @GetMapping("/userSession/invalidate/{sessionId}")
public ResponseEntity<String> expireAllSessions() { public String invalidateUserSession(
// Invalidate all sessions HttpServletRequest request, @PathVariable String sessionId) {
sessionRegistry.expireAllSessions(); sessionRegistry.expireSession(sessionId);
return ResponseEntity.ok("All sessions invalidated"); sessionRegistry.registerSession(request.getSession(false));
} return "redirect:/userSession";
@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

@ -142,25 +142,25 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
.filter(s -> !s.isExpired() && s.getPrincipalName().equals(principalName)) .filter(s -> !s.isExpired() && s.getPrincipalName().equals(principalName))
.toList() .toList()
.size(); .size();
boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled; boolean isAnonymousUserWithLogin = "anonymousUser".equals(principalName) && loginEnabled;
log.info( log.info(
"all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}", "all {} allNonExpiredSessions {} {} isAnonymousUserWithLogin {}",
all, all,
allNonExpiredSessions, allNonExpiredSessions,
getMaxUserSessions(), getMaxUserSessions(),
isAnonymousUserWithoutLogin); isAnonymousUserWithLogin);
if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) { if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithLogin) {
log.info("Session {} Expired=TRUE", session.getId()); log.info("Session {} Expired=TRUE", session.getId());
sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.expireSession(session.getId());
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
// if (allNonExpiredSessions > getMaxUserSessions()) { // if (allNonExpiredSessions > getMaxUserSessions()) {
// enforceMaxSessionsForPrincipal(principalName); // enforceMaxSessionsForPrincipal(principalName);
// } // }
} else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) { } else if (all >= getMaxUserSessions() && !isAnonymousUserWithLogin) {
enforceMaxSessionsForPrincipal(principalName); enforceMaxSessionsForPrincipal(principalName);
log.info("Session {} Expired=TRUE", session.getId()); log.info("Session {} Expired=TRUE", session.getId());
} else if (isAnonymousUserWithoutLogin) { } else if (isAnonymousUserWithLogin) {
sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.expireSession(session.getId());
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
} else { } else {

View File

@ -3,11 +3,8 @@ package stirling.software.SPDF.config.anonymus.session;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@ -29,35 +26,32 @@ import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
@Slf4j @Slf4j
public class SessionStatusController { public class SessionStatusController {
@Qualifier("loginEnabled")
private boolean loginEnabled;
@Autowired private SessionPersistentRegistry sessionPersistentRegistry; @Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired private SessionsInterface sessionInterface; @Autowired private SessionsInterface sessionInterface;
@Autowired private CustomHttpSessionListener customHttpSessionListener; @Autowired private CustomHttpSessionListener customHttpSessionListener;
// 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());
}
}
// list all sessions from authentication user, return String redirect userSession.html // list all sessions from authentication user, return String redirect userSession.html
@GetMapping("/userSession") @GetMapping("/userSession")
public String getUserSessions( public String getUserSessions(
HttpServletRequest request, Model model, Authentication authentication) { HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) { if ((authentication == null || !authentication.isAuthenticated()) && loginEnabled) {
return "redirect:/login"; return "redirect:/login";
} }
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
if (session != null) { if (session != null) {
Object principal = authentication.getPrincipal(); String principalName = null;
String principalName = UserUtils.getUsernameFromPrincipal(principal); if (authentication != null && authentication.isAuthenticated()) {
if (principalName == null) { Object principal = authentication.getPrincipal();
return "redirect:/login"; principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName == null) {
return "redirect:/login";
}
} else {
principalName = "anonymousUser";
} }
boolean isSessionValid = boolean isSessionValid =
@ -139,81 +133,4 @@ public class SessionStatusController {
return "redirect:/login"; return "redirect:/login";
} }
} }
// 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);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Object principalTest = authentication.getPrincipal();
String username = UserUtils.getUsernameFromPrincipal(principalTest);
List<SessionInformation> allSessions =
sessionPersistentRegistry.getAllSessions(username, false);
boolean isActivSession =
sessionPersistentRegistry.getAllSessions().stream()
.filter(
sessionEntity ->
session.getId().equals(sessionEntity.getSessionId()))
.anyMatch(sessionEntity -> !sessionEntity.isExpired());
int userSessions = allSessions.size();
int maxUserSessions = sessionInterface.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 invalid or expired");
} else if (session.getId() != null && isActivSession) {
return ResponseEntity.ok("Valid session: " + session.getId());
} else {
return ResponseEntity.ok(
"User: " + username + " has " + userSessions + " sessions");
}
} else {
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);
if (session != null) {
sessionPersistentRegistry.expireSession(session.getId());
return ResponseEntity.ok("Session invalidated");
} else {
return ResponseEntity.ok("No session to invalidate");
}
}
// 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();
Authentication auth = cxt.getAuthentication();
if (auth != null && auth.isAuthenticated()) {
Object principal = auth.getPrincipal();
String principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName.equals(username)) {
sessionPersistentRegistry.expireAllSessionsByUsername(username);
return ResponseEntity.ok("All sessions invalidated for user: " + username);
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized");
}
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized");
}
} }

View File

@ -287,6 +287,14 @@ database.notSupported=Diese Funktion ist für deine Datenbankverbindung nicht ve
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut. session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
session.refreshPage=Seite aktualisieren session.refreshPage=Seite aktualisieren
#################
# USER SESSION #
#################
userSession.title=Benutzersitzungen
userSession.header=Benutzersitzungen
userSession.maxUserSession=Wenn die maximale Anzahl Sitzungen für diesen Benutzer erreicht ist, können Sie hier andere Anmeldungen beenden, um auf diesem Gerät fortzufahren.
userSession.lastRequest=Letzte Aufrufe
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

View File

@ -287,6 +287,14 @@ database.notSupported=This function is not available for your database connectio
session.expired=Your session has expired. Please refresh the page and try again. session.expired=Your session has expired. Please refresh the page and try again.
session.refreshPage=Refresh Page session.refreshPage=Refresh Page
#################
# USER SESSION #
#################
userSession.title=User Sessions
userSession.header=User Sessions
userSession.maxUserSession=If the maximum number of sessions for this user is reached, you can end other logins here to continue on this device.
userSession.lastRequest=last Request
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

View File

@ -15,8 +15,7 @@
<p class="lead" th:if="${param.status == '404'}" th:text="#{error.404.1}"></p> <p class="lead" th:if="${param.status == '404'}" th:text="#{error.404.1}"></p>
<p class="lead" th:unless="${param.status == '404'}" th:text="#{error.404.2}"></p> <p class="lead" th:unless="${param.status == '404'}" th:text="#{error.404.2}"></p>
<p class="lead" th:if="${status == 417}"> <p class="lead" th:if="${status == 417}">
<a th:href="@{'/userSession'}" th:text="#{session.maxUserSession}">Max sessions reached for this user. To continue on this device, please close your <a th:href="@{'/userSession'}" th:text="#{userSession.maxUserSession}">Max sessions reached for this user.</a>
session in another browser.</a>
</p> </p>
<br> <br>
<h2 th:text="#{error.needHelp}"></h2> <h2 th:text="#{error.needHelp}"></h2>

View File

@ -3,7 +3,7 @@
xmlns:th="https://www.thymeleaf.org"> xmlns:th="https://www.thymeleaf.org">
<head> <head>
<th:block th:insert="~{fragments/common :: head(title=#{session.title}, header=#{session.header})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{userSession.title}, header=#{userSession.header})}"></th:block>
</head> </head>
<body> <body>
@ -16,21 +16,21 @@
<div class="col-md-9 bg-card"> <div class="col-md-9 bg-card">
<div class="tool-header"> <div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">key</span> <span class="material-symbols-rounded tool-header-icon organize">key</span>
<span class="tool-header-text" th:text="#{session.user}">User Session</span> <span class="tool-header-text" th:text="#{userSession.title}">User Session</span>
</div> </div>
<div class="bg-card mt-3 mb-3"> <div class="bg-card mt-3 mb-3">
<span th:text="#{session.maxUserSession}">Max sessions reached for this user. To continue on this device, please close your session in another browser.</span> <span th:text="#{userSession.maxUserSession}">Max sessions reached for this user.</span>
<table class="table table-striped table-hover mb-0"> <table th:unless="${#lists.isEmpty(sessionList)}" class="table table-striped table-hover mb-0">
<thead> <thead>
<tr> <tr>
<th scope="col" th:text="#{session.sessionId}">Session ID</th> <!-- <th scope="col" th:text="#{session.sessionId}">Session ID</th> -->
<th scope="col" th:text="#{session.lastRequest}">last Request</th> <th scope="col" th:text="#{userSession.lastRequest}">last Request</th>
<th scope="col" th:text="#{session.invalidate}">Logout</th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="userSession : ${sessionList}"> <tr th:each="userSession : ${sessionList}">
<td th:text="${userSession.getSessionId}"></td> <!-- <td th:text="${userSession.getSessionId}"></td> -->
<td th:text="${userSession.getLastRequest}"></td> <td th:text="${userSession.getLastRequest}"></td>
<td><a th:href="@{/userSession/invalidate/{id}(id=${userSession.getSessionId})}" <td><a th:href="@{/userSession/invalidate/{id}(id=${userSession.getSessionId})}"
class="btn btn-sm btn-danger"> class="btn btn-sm btn-danger">