diff --git a/server-session/README.md b/server-session/README.md index 094bb4372..0cd2d47fd 100644 --- a/server-session/README.md +++ b/server-session/README.md @@ -1,30 +1,32 @@ --- title: Server Session -category: Behavioral +category: Resource management language: en tag: + - Client-server - Cookies - Session management - State tracking + - Web development --- ## Also known as -Server-side session state pattern +* Server-Side Session Management ## Intent -Within the context of a client-server relationship, the server is responsible for storing session data in order to maintain state in an otherwise stateless environment. +Manage user session data on the server-side to maintain state across multiple client requests. ## Explanation Real-world example -> Consider a gaming website which stores user profile data such as username, password, high-score, hours played, etc. Since this website is accessed over the internet which uses the HTTP protocol, all requests sent to the server are stateless. In order for the page to display user relevant information without re-authenticating the user on every request a session must be created. Once the session is created the user can access the homescreen, statistics page, setting page, etc. and view profile specific data without needing to log in on every page request. +> Imagine a hotel where each guest is given a unique room key card upon check-in. This key card stores the guest's personal preferences, such as preferred room temperature, wake-up call times, and minibar choices. Whenever the guest interacts with hotel services, such as ordering room service or accessing the gym, the system retrieves their preferences using the information on the key card. The hotel’s central server maintains these preferences, ensuring consistent and personalized service throughout the guest's stay. Similarly, the Server Session design pattern manages user data on the server, providing a seamless experience across multiple interactions within a web application. In plain words -> Session data is stored on the server, whether in a database, text file or any other form of persistent storage, rather than the client's browser. +> The Server Session design pattern manages and stores user session data on the server to maintain state across multiple client requests in web applications. Wikipedia says @@ -32,190 +34,143 @@ Wikipedia says **Programmatic Example** -Consider a website in which a user can log in. Once logged in a session is created to maintain the specific user's data. +The Server Session design pattern is a behavioral design pattern that assigns the responsibility of storing session data on the server side. This pattern is particularly useful in the context of stateless protocols like HTTP where all requests are isolated events independent of previous requests. -First, we have the LoginHandler class, which creates a session identifier and sends it to the client as cookie. -Notice that not all data is sent to the client, the session creation time and user number are stored on the server side and thus cannot be accessed by the client. +In this pattern, when a user logs in, a session identifier is created and stored for future requests in a list. When a user logs out, the session identifier is deleted from the list along with the appropriate user session data. -The user logs in by visiting localhost:8080/login +Let's take a look at a programmatic example of the Server Session design pattern. -output: - -Login successful! -Session ID: 26434a9c-e734-4a64-97ce-7802b8de46bb +The main application starts a server and assigns handlers to manage login and logout requests. It also starts a background task to check for expired sessions. ```java -public class LoginHandler implements HttpHandler { +public class App { - private Map sessions; - private Map sessionCreationTimes; + private static Map sessions = new HashMap<>(); + private static Map sessionCreationTimes = new HashMap<>(); + private static final long SESSION_EXPIRATION_TIME = 10000; - public LoginHandler(Map sessions, Map sessionCreationTimes) { - this.sessions = sessions; - this.sessionCreationTimes = sessionCreationTimes; - } + public static void main(String[] args) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); - @Override - public void handle(HttpExchange exchange) { - // Generate session ID - String sessionID = UUID.randomUUID().toString(); + server.createContext("/login", new LoginHandler(sessions, sessionCreationTimes)); + server.createContext("/logout", new LogoutHandler(sessions, sessionCreationTimes)); - // Store session data (simulated) - int newUser = sessions.size() + 1; - sessions.put(sessionID, newUser); - sessionCreationTimes.put(sessionID, Instant.now()); - LOGGER.info("User " + newUser + " created at time " + sessionCreationTimes.get(sessionID)); + server.start(); - // Set session ID as cookie - exchange.getResponseHeaders().add("Set-Cookie", "sessionID=" + sessionID); + sessionExpirationTask(); + } - // Send response - String response = "Login successful!\n" + - "Session ID: " + sessionID; - try { - exchange.sendResponseHeaders(200, response.length()); - } catch (IOException e) { - LOGGER.error("An error occurred: ", e); - } - try(OutputStream os = exchange.getResponseBody()) { - os.write(response.getBytes()); - } catch(IOException e) { - LOGGER.error("An error occurred: ", e); - } - } -} -``` - -When the user logs out the session data is removed from storage using the LogoutHandler class. - -The user logs out by visiting localhost:8080/logout - -output: - -Logout successful! -Session ID: 26434a9c-e734-4a64-97ce-7802b8de46bb - -```java -public class LogoutHandler implements HttpHandler { - - private Map sessions; - private Map sessionCreationTimes; - - public LogoutHandler(Map sessions, Map sessionCreationTimes) { - this.sessions = sessions; - this.sessionCreationTimes = sessionCreationTimes; - } - - @Override - public void handle(HttpExchange exchange) { - // Get session ID from cookie - String sessionID = exchange.getRequestHeaders().getFirst("Cookie").replace("sessionID=", ""); - String currentSessionID = sessions.get(sessionID) == null ? null : sessionID; - - // Send response - - String response = ""; - if(currentSessionID == null) { - response += "Session has already expired!"; - } else { - response = "Logout successful!\n" + - "Session ID: " + currentSessionID; - } - - //Remove session - if(currentSessionID != null) - LOGGER.info("User " + sessions.get(currentSessionID) + " deleted!"); - else - LOGGER.info("User already deleted!"); - sessions.remove(sessionID); - sessionCreationTimes.remove(sessionID); - - try { - exchange.sendResponseHeaders(200, response.length()); - } catch(IOException e) { - LOGGER.error("An error has occurred: ", e); - } - - try(OutputStream os = exchange.getResponseBody()) { - os.write(response.getBytes()); - } catch(IOException e) { - LOGGER.error("An error has occurred: ", e); - } - } -} -``` - -Sessions are often given a maximum time in which they will be maintained. The sessionExpirationTask() creates a thread which runs every 1 minute to check for sessions that have exceeded the maximum amount of time, in this case 1 minute and removes the session data from the server's storage. - -```java - private static void sessionExpirationTask() { + private static void sessionExpirationTask() { new Thread(() -> { - while (true) { - try { - LOGGER.info("Session expiration checker started..."); - Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time - Instant currentTime = Instant.now(); - synchronized (sessions) { - synchronized (sessionCreationTimes) { - Iterator> iterator = sessionCreationTimes.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) { - sessions.remove(entry.getKey()); - iterator.remove(); - } - } - } + while (true) { + try { + Thread.sleep(SESSION_EXPIRATION_TIME); + Instant currentTime = Instant.now(); + synchronized (sessions) { + synchronized (sessionCreationTimes) { + Iterator> iterator = + sessionCreationTimes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) { + sessions.remove(entry.getKey()); + iterator.remove(); } - LOGGER.info("Session expiration checker finished!"); - } catch (InterruptedException e) { - LOGGER.error("An error occurred: ", e); - Thread.currentThread().interrupt(); + } } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } + } }).start(); + } } ``` +The LoginHandler is responsible for handling login requests. When a user logs in, a session identifier is created and stored for future requests in a list. + +```java +public class LoginHandler { + + private Map sessions; + private Map sessionCreationTimes; + + public LoginHandler(Map sessions, Map sessionCreationTimes) { + this.sessions = sessions; + this.sessionCreationTimes = sessionCreationTimes; + } + + public void handle(HttpExchange exchange) { + // Implementation of the handle method + } +} +``` + +The LogoutHandler is responsible for handling logout requests. When a user logs out, the session identifier is deleted from the list along with the appropriate user session data. + +```java +public class LogoutHandler { + + private Map sessions; + private Map sessionCreationTimes; + + public LogoutHandler(Map sessions, Map sessionCreationTimes) { + this.sessions = sessions; + this.sessionCreationTimes = sessionCreationTimes; + } + + public void handle(HttpExchange exchange) { + // Implementation of the handle method + } +} +``` + +This is a basic example of the Server Session design pattern. The actual implementation of the `handle` methods in the `LoginHandler` and `LogoutHandler` classes would depend on the specific requirements of your application. + ## Class diagram ![Server Session class diagram](./etc/server-session.urm.png "Server Session class diagram") ## Applicability -Use the Adapter pattern when - -* When a user logs into a website or web application, and you want to keep track of their authentication status. -* In e-commerce websites when you want to maintain the contents of a user's shopping cart across different pages and visits. -* When you want to store user preferences and settings, such as language preferences, theme choices, or any other customizable options. -* When you want to keep track of user activity and behavior on a website for the sake of analytics purposes. +* Use when building web applications that require maintaining user state information across multiple requests. +* Suitable for applications needing to track user interactions, preferences, or authentication state. +* Ideal for scenarios where client-side storage is insecure or insufficient. ## Tutorials -* [Web Dev Simplified](https://www.youtube.com/watch?v=GihQAC1I39Q&pp=ygUMaHR0cCBzZXNzaW9u) -* [Hackersploit](https://www.youtube.com/watch?v=zHBpJA5XfDk) +* [JavaScript Cookies vs Local Storage vs Session Storage - Web Dev Simplified](https://www.youtube.com/watch?v=GihQAC1I39Q&pp=ygUMaHR0cCBzZXNzaW9u) +* [Web App Pentesting - HTTP Cookies & Sessions - Hackersploit](https://www.youtube.com/watch?v=zHBpJA5XfDk) + +## Known Uses + +* Java EE applications using HttpSession for session management. +* Spring Framework's `@SessionAttributes` for handling user session data. +* Apache Tomcat's session management mechanism. ## Consequences -Pros +Benefits: -* HTTP sessions are typically not implemented using one thread per session, but by means of a database with information about the state of each session. The advantage with multiple processes or threads is relaxed complexity of the software, since each thread is an instance with its own history and encapsulated variables. -* Server-side session management is generally more secure than client-side alternatives like storing session data in cookies. -* Server-side session management can scale more easily, especially when using distributed caching systems or databases. +* Simplifies client-side logic by offloading state management to the server. +* Enhances security by storing sensitive information server-side. +* Supports complex state management scenarios like multistep forms or shopping carts. -Cons +Trade-offs: -* Large overhead in terms of system resources, and that the session may be interrupted if the system is restarted. -* Can become difficult to handle in conjunction with load-balancing/high-availability systems and are not usable at all in some embedded systems with no storage. -* If the server hosting the session data goes down or experiences issues, it can disrupt the entire application's functionality, potentially leading to session data loss and user inconvenience. +* Increases server memory usage due to storage of session data. +* Requires session management logic to handle session timeouts and data persistence. +* Potential scalability issues with high user concurrency. -## Real-world examples +## Related Patterns -* Express.js Session Middleware -* Spring Session in Spring Boot -* Django Session Framework -* Java Servlet Session Management +* [State](https://java-design-patterns.com/patterns/state/): Manages state-specific behavior, which can be utilized within session management to handle different user states. +* [Proxy](https://java-design-patterns.com/patterns/proxy/): Can be used to add a layer of control over session data access. +* [Singleton](https://java-design-patterns.com/patterns/singleton/): Often used to create a single instance of a session manager. ## Credits -* [Session(Computer Science)](https://en.wikipedia.org/wiki/Session_(computer_science) \ No newline at end of file +* [Core J2EE Patterns: Best Practices and Design Strategies](https://amzn.to/4cAbDap) +* [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0pvKI) +* [Patterns of Enterprise Application Architecture](https://amzn.to/3WfKBPR) diff --git a/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java b/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java index e7e4db42f..db5445f88 100644 --- a/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java +++ b/server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java @@ -30,7 +30,6 @@ import static org.mockito.Mockito.when; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -64,7 +63,7 @@ public class LoginHandlerTest { } @Test - public void testHandle() throws IOException { + public void testHandle() { //assemble ByteArrayOutputStream outputStream = diff --git a/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java b/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java index 5d7202e9f..1b9d817b0 100644 --- a/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java +++ b/server-session/src/test/java/com.iluwatar.sessionserver/LogoutHandlerTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.when; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -67,7 +66,7 @@ public class LogoutHandlerTest { } @Test - public void testHandler_SessionNotExpired() throws IOException { + public void testHandler_SessionNotExpired() { //assemble sessions.put("1234", 1); //Fake login details since LoginHandler isn't called @@ -83,12 +82,12 @@ public class LogoutHandlerTest { //assert String[] response = outputStream.toString().split("Session ID: "); Assertions.assertEquals("1234", response[1]); - Assertions.assertFalse(sessions.containsKey(response)); - Assertions.assertFalse(sessionCreationTimes.containsKey(response)); + Assertions.assertFalse(sessions.containsKey(response[1])); + Assertions.assertFalse(sessionCreationTimes.containsKey(response[1])); } @Test - public void testHandler_SessionExpired() throws IOException { + public void testHandler_SessionExpired() { //assemble ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -101,7 +100,5 @@ public class LogoutHandlerTest { //assert String[] response = outputStream.toString().split("Session ID: "); Assertions.assertEquals("Session has already expired!", response[0]); - Assertions.assertFalse(sessions.containsKey(response)); - Assertions.assertFalse(sessionCreationTimes.containsKey(response)); } }