CodeGym /Kursy /Spring Security /Session model: convenience and limitations

Session model: convenience and limitations

Spring Security
Poziom 9, Lekcja 4
Dostępny

1. Evaluating the session model

Now that we have already seen the full login -> series of requests -> logout -> timeout cycle, we can stop seeing the session as just “mechanics under the hood” and evaluate it as an engineering solution. The lifecycle is where it becomes clearest where the session-based model is convenient, and where you have to pay for it.

If you are just getting started, it is very easy to fall into one of two extremes. The first is: “the session is magic, I do not touch it.” The second is: “the session is outdated, let us do without it.” Both extremes usually end the same way: unexpected 401/403 responses, broken scenarios, and the feeling that Spring Security is living its own life.

The session model is neither magic nor a relic of the past. It is a way to keep the authenticated state on the server and restore it between related requests. As long as the browser sends the session id, the server can return the user’s SecurityContext without forcing a new login.

This is where both the advantages and the cost of the model come from: convenient continuity for the browser, but along with it — server memory, timeouts, logout semantics, and scaling concerns. Below, we will look at where this model feels natural and where it starts to get in the way.

2. Where a session is convenient

The session model is convenient where the user makes many related requests and expects the system to “remember” the state between them. The most obvious example is a browser scenario: the user opened the application, signed in, and then navigates through their personal area. They do not need to prove who they are again every time. In your mind, this feels like “I am already signed in”, and on the server side it feels like “I have a session id, and from it I can find the stored SecurityContext”.

In human terms, it is like a wristband at a water park: the ticket is checked once, and after that the system recognizes you by the issued identifier. JSESSIONID in this analogy is that identifier; HttpSession is the server-side record; SecurityContext is the mark that you are already authenticated and what permissions you have.

In our educational project, Secure Content Platform API, this is especially convenient in the private area. The user signs in — and then can calmly make a series of calls:

GET /api/me — “Who am I?”

GET /api/me/profile — “My profile”

GET /api/drafts — “My drafts”

In all these requests, controllers do not “remember the last time”. They receive an already restored Authentication/Principal within the current request because Spring Security restored the SecurityContext from the session. For a developer, this looks like a very pleasant model: business code does not have to deal with re-authentication at every step.

Here is a simple controller example that “uses the convenience” of a session: the current user is available without ritual gymnastics around login on every request.

package com.example.securecontent.me;

import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class MeController {

    // Spring Security has already restored the SecurityContext from the session,
    // so Principal is available directly as a method parameter.
    @GetMapping("/api/me")
    String me(Principal principal) {
        // In a real application, this endpoint is usually protected,
        // so principal is expected not to be null here.
        return "Hello, " + principal.getName(); // Hello, alice
    }
}

Notice that this method has neither a password, nor a login form, nor explicit cookie reading. And this is not because “Spring hides everything”, but because you already understand the mechanism: the session is tied to JSESSIONID, Spring Security restored the context, and there it is — Principal. That is the strong side of the session-based model: it makes a “personal area” feel natural.

Another convenience is server-side control. The server can end the authenticated state through logout or through timeout, and the user becomes anonymous again. This is not always “pleasant for the user”, but it is manageable. And manageability is an important part of security and operations, even if you are only working on a learning project for now.

3. The cost: state is stored on the server

When you store state on the server, you are literally saying: “For every active client, I have some piece of memory (and metadata) somewhere on the server that needs to be maintained.” And this is where the limitations begin — the ones you really want to ignore, but that do not let themselves be ignored.

The first limitation is memory and resources. A session does not have to be huge by itself, but its existence means the server needs to keep a structure like “session id → session data” somewhere. Even if you do not explicitly put anything into the session, Spring Security puts the SecurityContext there. In a small educational project, that is not scary, but in a real application, “many active users” turns into “many active sessions”. And many active sessions are no longer philosophy — they are memory and load numbers.

The second limitation is the session lifetime becomes part of the system behavior. If the session is too long, the user will “stay signed in” for a long time, even if they forgot to close the laptop in a coffee shop (purely hypothetically, of course). If the session is too short, the user will constantly get kicked out and be annoyed. And this is not just UX — it is also support: “Why am I being logged out every 2 minutes?” can quickly become your favorite task of the week.

In Spring Boot, the session timeout can be configured in the settings. For a learning project, this is a great way to consciously see that the session is “not eternal” and lives by rules.

# application.yml
server:
  servlet:
    session:
      # Inactivity timeout: if there are no requests with this session for N minutes, it expires.
      timeout: 15m

What matters here is not so much the value as the idea: timeout is not “some magic setting down there”. It is part of your application model. And if you do not control it, it will control you.

The third limitation is multiple application instances. Even if you have not yet deployed anything “across several instances”, it is useful to understand the problem at the level of the idea. Imagine your application is running in two copies: app-1 and app-2. The user signed in on app-1, received a JSESSIONID, and the next request accidentally landed on app-2. If app-2 does not know about this session, it cannot restore the SecurityContext, and the user suddenly becomes anonymous. “But I just signed in!” the user says. “I am seeing you for the first time,” says app-2. And both will be right in their own way.

You can picture it like this:

sequenceDiagram
    %% Problem: the session lives on a specific instance, and another instance does not know about it.
    participant B as Browser
    participant A1 as "App instance #1"
    participant A2 as "App instance #2"

    B->>A1: "POST /login (successful)"
    A1-->>B: Set-Cookie: JSESSIONID=abc

    B->>A2: "GET /api/me (Cookie: abc)"
    A2-->>B: "401 (does not know session abc)"

We will not go into how exactly this is solved in infrastructure today (that is a separate big topic). But it is important to see the boundary: the session-based model naturally works in a “one server/one copy” world, and when you scale, you inevitably start thinking about how requests reach the right instance and where sessions live.

The fourth limitation is the session as a “dump” for data. Very often, beginner developers see HttpSession as a convenient pocket: “Oh, I can put anything there!” Technically, you can. The setAttribute method will not stop you. But architecturally, this is almost always a bad idea, because you turn the session into a hidden storage for business data that:

  • is hard to test (it is “somewhere out there”);
  • is hard to understand (why is it suddenly there or suddenly gone);
  • is hard to maintain (timeout and invalidation start breaking business scenarios).

In educational examples, a “visit counter” is fine — it shows the mechanism. In a real application, “stuff the user profile, draft list, and current draft into the session because it is faster” is almost guaranteed pain.

To make the difference easier to feel, let us look at two fragments: a bad one and a good one (the good one is not always shorter, but it is more honest).

A bad habit is to store a “piece of the domain” in the session, as if it were a cache:

package com.example.securecontent.badidea;

import com.example.securecontent.profile.UserProfileDto;
import jakarta.servlet.http.HttpSession;

class SessionStuffingExample {

    void dontDoThis(HttpSession session, UserProfileDto profile) {
        // It is tempting to do this, but it ties business logic to the session lifecycle.
        // Timeout/logout/application restart can easily "wipe out" this data.
        session.setAttribute("profile", profile); // "convenient", but very fragile
    }
}

Yes, it works. And then you add logout, timeout, a second application instance, or you simply change the DTO — and suddenly the “magic disappearance of data” begins. The session is good as a mechanism for keeping the security context, but as a store for business data it almost always turns into a “black box”.

The fifth cost is statefulness affects diagnostics. In a stateless world (we are not discussing it today, just as a thought), every request is self-sufficient. In a session-based world, a request can be “with context” or “without context”, and that affects the result. So when debugging, you need to be able to ask yourself: “Did this request even arrive with the correct session?”

4. Manageability: the session under control

After the previous section, it is easy to come to the wrong conclusion: “Well then, sessions are just problems.” That is not true. In fact, the problems do not appear because the session exists, but because we treat it as uncontrolled magic. So here we will establish several practical principles that make the session-based model predictable and calm.

The first principle is the session should be “thin”. In the ideal world, you store in it only what is really needed to keep authentication state alive (and a minimal technical context). Everything else — business data — lives in your normal application model: in the database, in services, in request DTOs. The less you put of your own stuff into the session, the fewer surprises you will get from timeout, logout, and other life events.

The second principle is logout must be explicit and end the server-side state, not merely “hope that the cookie disappears”. You have already seen that logout behavior can be controlled, and in the project this helps make the behavior honest: the user logs out — the session is invalidated, the cookie is removed.

Configuration example (we did something similar in the previous lecture, but here the idea of “manageability” is what matters):

package com.example.securecontent.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

@Configuration
class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Configure authorization at the HTTP request level.
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**", "/debug/**").permitAll() // Public endpoints and debug area
                .anyRequest().authenticated() // Everything else requires authentication (that is, a session)
            )
            // Enable formLogin: after a successful login, Spring will create/use a session and put the SecurityContext there.
            .formLogin(Customizer.withDefaults())
            // Logout should invalidate server-side state, not just "ask the browser to forget the cookie".
            .logout(logout -> logout
                .invalidateHttpSession(true) // Kill the HttpSession on the server
                .deleteCookies("JSESSIONID") // Remove the session identifier from the client
            );

        return http.build();
    }
}

The third principle is the timeout should be meaningful. Even in a learning project, it is useful to set a clear value and see with your own hands that “after N minutes of inactivity” the user becomes anonymous again. This gives you a real understanding of why sometimes “the user disappeared”: not because the controller forgot, but because the session disappeared, and therefore the source from which the SecurityContext was restored disappeared too.

The fourth principle is the session does not replace authorization. This is a surprisingly common thinking mistake: “If a person signed in, then they can do anything.” No, signing in is only the fact of authentication. Access rules (roles/authorities and request-level configuration) remain mandatory. The session makes the user “recognized”, but it does not make them “omnipotent” automatically.

In short: the session answers “who are you?”, and access rules answer “what is allowed for you?” And these are different questions, even if they sometimes try to blend into one mess in the mind of a beginner developer.

5. The session in our project

In the end-to-end project Secure Content Platform API, the convenience of the session model is visible when you look at personal endpoints as one continuous user scenario rather than as separate HTTP calls. The user logs into the system — and then lives in the personal area. They read the profile, look at drafts, edit content. At the same time, controllers and services should not know how exactly login was performed. They should simply receive the current user and apply access rules.

For example, a drafts list endpoint can look very simple because “who is making the request” is already known:

package com.example.securecontent.content;

import java.security.Principal;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class DraftController {

    @GetMapping("/api/drafts")
    List<String> drafts(Principal principal) {
        // Principal comes from the SecurityContext, which Spring Security gets from the session.
        // That is, we do not send the login/password on every request — and that is the convenience of the stateful model.
        return List.of("drafts of " + principal.getName()); // drafts of alice
    }
}

Yes, this is still just a teaching stub. But it perfectly emphasizes today’s topic: in the session-based branch, you get a feeling of “continuing the conversation”. One login — and then you keep seeing the same user everywhere as long as the session is alive.

Now for the limitations. If you suddenly decide to make a smart draft cache in the session so you “do not hit the backend too often”, you will almost certainly create problems for yourself. Why? Because:

- the user may open two windows, make changes, and see inconsistency;

- the session may expire, and the “cache” will disappear;

- logout will clear the session, and the state will “vanish” unexpectedly;

- with multiple application instances, you will get different sessions (depending on request routing).

So the correct learning takeaway is this: the session helps keep security state alive, but it should not become a store for domain data.

And one more important idea for our course specifically. Even if it seems that the session “solves access”, it only solves it at the first step: it makes the user authenticated. But the endpoints /api/editor/** and /api/admin/** still have to check roles/permissions. And that is great, because otherwise we would have a “system: signed in — and now anything goes”, and that is no longer a content platform, but a room without doors.

6. When a session gets in the way

The session model starts to get in the way not because it is “bad”, but because requirements and the environment change. Very roughly, you can say this: if your client is a browser and the user performs a series of related actions, a session almost always feels natural. If there are many different clients, if the application is frequently scaled horizontally, if strict predictability of every request independent of history matters — you begin to feel the cost of server-side state.

In a learning project, this shows up gently: you may hit a timeout, forget that restarting the application “kills” sessions, and be surprised why you were logged out. These are useful “small pains” that build the right engineering discipline: you start to understand that state lives on the server and has a lifecycle.

In the real world, the limitations become more tangible. You want to run two copies of the application (for example, so it does not go down completely when one instance restarts). Or you want to deploy the application in an environment where requests may go to different instances. Or you want clients to be not only a browser, but something else too. And then “the session as the only way to keep login” starts requiring additional solutions.

But — and this is essential — that does not make the session “bad”. It makes it a tool with a scope of applicability. Like a hammer: an excellent tool when you are driving nails. But trying to screw in screws with a hammer is not “the hammer is outdated”; it is “we chose the wrong tool”.

So the mature junior/junior+ position is this: a session is a normal model, especially for browser-oriented scenarios. It just has limitations, and it is precisely on these limitations that the difference becomes clear from models where the client confirms itself separately on each request and the server does not keep the same continuity on its side.

7. Typical mistakes when working with a session

Mistake #1: declaring the session model “bad by definition”.
Sometimes a beginner hears the phrase “session is outdated” somewhere and starts treating it as a law of nature. As a result, they try to throw away the session without understanding what exactly it made convenient and what problems it solved. It is much more useful to think not in ideology, but in context: if the user makes many related actions from one browser flow, a session is a natural choice, and that is normal.

Mistake #2: ignoring server-side overhead and the session lifecycle.
While the project is small, it seems that sessions “cost nothing”. And then suddenly a timeout appears, logout appears, the application restarts, and the user “gets lost”. This is not a Spring Security bug and not “the controller forgot the user” — it is the normal cost of server-side state. A useful habit is to always keep in mind that the session may disappear, and therefore the SecurityContext may not be restored.

Mistake #3: dumping everything into HttpSession and turning it into a junk drawer.
session.setAttribute is very tempting: put the profile there, drafts, the current filter, anything. The problem is that the session stops being a thin part of the security mechanism and becomes hidden storage for business data. Then logout and timeout start breaking functionality, and the code becomes harder to understand and test. Session state should remain short and predictable.

Mistake #4: thinking “signed in” = “allowed to do anything”.
The session gives you an authenticated user, but it does not replace authorization. Even if the user is “recognized”, they still need access rules: request-level restrictions, roles, authorities. Otherwise the personal area, editor area, and admin panel turn into one big shared room. The correct mental model is: the session helps answer “who are you?”, and access rules answer “what is allowed for you?”.

Mistake #5: confusing the session problem with the cookie problem.
Sometimes a developer sees JSESSIONID and starts treating the cookie as the user’s “credentials”. Then they start treating it like a password: logging it, copying it, manually inserting it into requests, discussing it in the team chat. In reality, the cookie is only an identifier that connects the client to the server-side session. It is important, yes, but it is not a “login” and not a “role”. And most importantly, do not turn it into an object of worship or fear: it is enough to understand that it is the key to server-side state, and it should be handled carefully.

1
Zadanie
Spring Security, poziom 9, lekcja 4
Niedostępne
Jedna sesja dla strefy prywatnej, ale nie dla administracyjnej
Jedna sesja dla strefy prywatnej, ale nie dla administracyjnej
1
Zadanie
Spring Security, poziom 9, lekcja 4
Niedostępne
Monitorowanie parametrów sesji serwerowej
Monitorowanie parametrów sesji serwerowej
1
Ankieta/quiz
Sesje HTTP, poziom 9, lekcja 4
Niedostępny
Sesje HTTP
Sesje i Spring Security
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION