Skip to content
Chapter 36 · Spring Boot

Spring Security Part 3: Form Login (Stateful) ​

Part 2 stored users. This part lets them log in with form login, the first authentication method from the list in part 1, and adds authorization rules on top.

Form login ​

Form login is a stateful authentication method: the server keeps the user's authentication state, a session, so the user does not send a username and password with every request.

  1. The user enters username and password in an HTML login form.
  2. On success, the server creates an HTTP session and sends its id back in a cookie, JSESSIONID.
  3. Later requests send only the JSESSIONID cookie. The server looks up the session; if it exists and has not expired, the request is served. Otherwise the user is redirected to the login page.

It is Spring Security's default method: unless you configure JWT or something else, you get form login. Default URLs:

  • login: /login (the page and the form's POST)
  • logout: /logout (ends the session before it expires)

Session timeout ​

By default a session lives for 30 minutes of inactivity (the servlet container's default, Tomcat here). To change it:

properties
server.servlet.session.timeout=1m

The timeout counts inactivity: each request extends it, so an active user is never logged out in the middle of working.

Storing sessions in a database ​

By default sessions live in the server's memory. To store them in a relational database, add Spring Session JDBC:

xml
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>
properties
spring.session.jdbc.initialize-schema=always
server.servlet.session.timeout=5m

Having the dependency is enough to switch storage. (Older guides also set spring.session.store-type=jdbc; Spring Boot 3 removed that property.) initialize-schema=always makes Spring create the tables:

  • SPRING_SESSION: one row per session, with the session id, creation and last-access times, MAX_INACTIVE_INTERVAL (300 seconds here), the expiry time and the username (PRINCIPAL_NAME). The expiry time moves forward with each request.
  • SPRING_SESSION_ATTRIBUTES: the session's contents, including SPRING_SECURITY_CONTEXT.

With Spring Session, the cookie is called SESSION instead of JSESSIONID.

The flow ​

1. First login (the user exists, no session yet) ​

The default login page's form sends username, password and a hidden CSRF token. UsernamePasswordAuthenticationFilter handles POST /login and runs the flow from part 1: DaoAuthenticationProvider loads the user and checks the password. On success, the authenticated Authentication is put in the SecurityContext, which is saved in the HTTP session, and the response sets the session cookie. (Spring gives the user a new session id at login, to prevent session fixation.)

Where does it redirect?

  • If the user first asked for a protected page, say /users, and was sent to the login page, they go back to /users.
  • If they opened /login directly, they go to the default page, /.

A wrong password redirects to /login?error.

2. Later requests ​

SecurityContextHolderFilter finds the session from the cookie and loads the stored SecurityContext. No username or password is checked again. If there is no valid session, the user is sent to the login page.

All of this is framework code; nothing to write. The default configuration, in Spring Boot's SpringBootWebSecurityConfiguration:

java
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin(withDefaults());
    http.httpBasic(withDefaults());
    return http.build();
}

Every request must be authenticated, with form login and basic authentication enabled. For the flow above, all you need is the dependency and a user (for example in application.properties).

Customizing: your own SecurityFilterChain ​

To change the defaults (a custom login page, some endpoints open to everyone), define your own SecurityFilterChain bean; Spring Boot's default then steps aside:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/users").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

formLogin(Customizer.withDefaults()) keeps the default pages; .formLogin(form -> form.loginPage("/my-login")) would use your own. For JWT and other methods, this bean is where you configure them instead of form login.

Authorization ​

After authentication, each request still passes the AuthorizationFilter, which checks whether the user may access the resource. By default Spring Security imposes no role restrictions: any authenticated user can reach any endpoint. You add them yourself:

java
http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/users").hasRole("USER")
        .anyRequest().authenticated()
    )
    .formLogin(Customizer.withDefaults());
properties
spring.security.user.name=user
spring.security.user.password=pass
spring.security.user.roles=USER
  • hasRole("USER") checks for the authority ROLE_USER; the ROLE_ prefix is added for you.
  • The AuthorizationFilter compares the roles in the SecurityContext with what the endpoint requires. If they do not match: 403 Forbidden.
  • Role names are just strings (ADMIN, USER, ...); choose meaningful ones. A user can have several roles, and an endpoint can accept several:
java
.requestMatchers("/users").hasAnyRole("USER", "ADMIN")
properties
spring.security.user.roles=USER,ADMIN

Limiting sessions per user ​

A user can log in from several browsers, each with its own session. To allow only one:

java
http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/users").hasRole("USER")
        .anyRequest().authenticated()
    )
    .sessionManagement(session -> session
        .maximumSessions(1)
        .maxSessionsPreventsLogin(true)
    )
    .formLogin(Customizer.withDefaults());

A second login from another browser now fails with "Maximum sessions of 1 for this principal exceeded".

Also register this bean:

java
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

Spring Security counts sessions in a registry, and this bean tells the registry when a session ends. Without it, a user who logs out still counts as logged in and cannot log in again until the old session expires.

Session creation policies ​

java
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
PolicyBehavior
IF_REQUIRED (default)A session is created only when needed. A public API that needs no authentication gets no session.
ALWAYSA session is always created (or reused), even for public APIs.
NEVERSpring Security never creates a session, but uses one if it already exists.
STATELESSNo session at all. For stateless methods such as JWT.

IF_REQUIRED is the sensible default: a public, informational API may be hit millions of times, and keeping sessions for those calls is wasted memory.

Drawbacks of form login ​

  1. CSRF and session hijacking. Because the browser sends the session cookie automatically, form login is exposed to CSRF (see the attacks chapter). CSRF protection is on by default for form login; do not disable it with .csrf(csrf -> csrf.disable()). And if the session id is stolen, an attacker can use it to act as the user (session hijacking).
  2. Session management overhead, which makes scaling harder in a distributed system: each server must know every session.
  3. Database or cache load: with several servers, sessions are usually kept in a database or cache, which costs memory and a lookup on every request, adding latency.

Next: The session makes form login stateful, which is harder to scale. Part 4 covers Basic authentication, a stateless method that sends the credentials with every request.

Check yourself ​

  1. Why is form login called stateful?
  2. What is Spring Security's default authentication method, and what are the default login and logout URLs?
  3. How long does a session last by default, and how do you change it? Is it measured from login or from the last request?
  4. What do you need to store sessions in a database, and which tables appear?
  5. After a successful POST /login, where is the user redirected?
  6. On later requests, which filter restores the authenticated user, and from where?
  7. What does Spring Boot's default SecurityFilterChain configure?
  8. What does hasRole("USER") check, and what happens if the role is missing?
  9. With maximumSessions(1).maxSessionsPreventsLogin(true), what happens on a second login? Which bean is also needed, and why?
  10. Explain IF_REQUIRED, ALWAYS, NEVER and STATELESS.
  11. Give three drawbacks of form login.
Answers
  1. The server keeps the authentication state in a session.
  2. Form login; /login and /logout.
  3. 30 minutes (Tomcat); server.servlet.session.timeout. From the last request (inactivity).
  4. The spring-session-jdbc dependency (plus spring.session.jdbc.initialize-schema=always to create tables): SPRING_SESSION and SPRING_SESSION_ATTRIBUTES.
  5. To the protected page they originally asked for, or to / if they opened /login directly.
  6. SecurityContextHolderFilter, from the SecurityContext stored in the HTTP session.
  7. All requests authenticated, with form login and basic authentication.
  8. The authority ROLE_USER in the security context; 403 Forbidden.
  9. It fails: "Maximum sessions of 1 for this principal exceeded". HttpSessionEventPublisher, so ended sessions are removed from the registry.
  10. IF_REQUIRED: create only when needed (default). ALWAYS: always create. NEVER: never create, but use an existing one. STATELESS: no session at all.
  11. CSRF and session hijacking; session management overhead in distributed systems; database or cache load for shared sessions.