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

View File

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

View File

@ -1,9 +1,10 @@
package stirling.software.SPDF.config.anonymus.session;
import java.util.List;
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.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -12,72 +13,44 @@ import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
@Controller
@Slf4j
public class AnonymusSessionStatusController {
@Autowired private AnonymusSessionListener sessionRegistry;
@GetMapping("/session/status")
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 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) {
@GetMapping("/userSession")
public String getUserSessions(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session != null) {
// Invalidate current session
sessionRegistry.expireFirstSession(session.getId());
log.info("Session invalidated: {}", session.getId());
// return ResponseEntity.ok("Session invalidated");
} else {
log.info("No session to invalidate");
// return ResponseEntity.ok("No session to invalidate");
boolean isSessionValid =
sessionRegistry.getAllNonExpiredSessions().stream()
.allMatch(
sessionEntity ->
sessionEntity.getSessionId().equals(session.getId()));
// 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:/";
}
@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);
@GetMapping("/userSession/invalidate/{sessionId}")
public String invalidateUserSession(
HttpServletRequest request, @PathVariable String sessionId) {
sessionRegistry.expireSession(sessionId);
sessionRegistry.registerSession(request.getSession(false));
return "redirect:/userSession";
}
}

View File

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

View File

@ -3,11 +3,8 @@ package stirling.software.SPDF.config.anonymus.session;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Qualifier;
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.stereotype.Controller;
import org.springframework.ui.Model;
@ -29,35 +26,32 @@ import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
@Slf4j
public class SessionStatusController {
@Qualifier("loginEnabled")
private boolean loginEnabled;
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired private SessionsInterface sessionInterface;
@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
@GetMapping("/userSession")
public String getUserSessions(
HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
if ((authentication == null || !authentication.isAuthenticated()) && loginEnabled) {
return "redirect:/login";
}
HttpSession session = request.getSession(false);
if (session != null) {
Object principal = authentication.getPrincipal();
String principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName == null) {
return "redirect:/login";
String principalName = null;
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName == null) {
return "redirect:/login";
}
} else {
principalName = "anonymousUser";
}
boolean isSessionValid =
@ -139,81 +133,4 @@ public class SessionStatusController {
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.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 #
#############

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.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 #
#############
@ -1428,7 +1436,7 @@ cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
cookieBanner.preferencesModal.necessary.title.2=Always Enabled
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off.
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off.
cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.

View File

@ -15,8 +15,7 @@
<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: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
session in another browser.</a>
<a th:href="@{'/userSession'}" th:text="#{userSession.maxUserSession}">Max sessions reached for this user.</a>
</p>
<br>
<h2 th:text="#{error.needHelp}"></h2>

View File

@ -3,7 +3,7 @@
xmlns:th="https://www.thymeleaf.org">
<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>
<body>
@ -16,21 +16,21 @@
<div class="col-md-9 bg-card">
<div class="tool-header">
<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 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>
<table class="table table-striped table-hover mb-0">
<span th:text="#{userSession.maxUserSession}">Max sessions reached for this user.</span>
<table th:unless="${#lists.isEmpty(sessionList)}" class="table table-striped table-hover mb-0">
<thead>
<tr>
<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="#{session.invalidate}">Logout</th>
<!-- <th scope="col" th:text="#{session.sessionId}">Session ID</th> -->
<th scope="col" th:text="#{userSession.lastRequest}">last Request</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr th:each="userSession : ${sessionList}">
<td th:text="${userSession.getSessionId}"></td>
<!-- <td th:text="${userSession.getSessionId}"></td> -->
<td th:text="${userSession.getLastRequest}"></td>
<td><a th:href="@{/userSession/invalidate/{id}(id=${userSession.getSessionId})}"
class="btn btn-sm btn-danger">