Appearance
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.
- The user enters username and password in an HTML login form.
- On success, the server creates an HTTP session and sends its id back in a cookie,
JSESSIONID. - Later requests send only the
JSESSIONIDcookie. 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'sPOST) - 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=1mThe 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=5mHaving 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, includingSPRING_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
/logindirectly, 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=USERhasRole("USER")checks for the authorityROLE_USER; theROLE_prefix is added for you.- The
AuthorizationFiltercompares the roles in theSecurityContextwith 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,ADMINLimiting 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))| Policy | Behavior |
|---|---|
IF_REQUIRED (default) | A session is created only when needed. A public API that needs no authentication gets no session. |
ALWAYS | A session is always created (or reused), even for public APIs. |
NEVER | Spring Security never creates a session, but uses one if it already exists. |
STATELESS | No 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
- 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). - Session management overhead, which makes scaling harder in a distributed system: each server must know every session.
- 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
- Why is form login called stateful?
- What is Spring Security's default authentication method, and what are the default login and logout URLs?
- How long does a session last by default, and how do you change it? Is it measured from login or from the last request?
- What do you need to store sessions in a database, and which tables appear?
- After a successful
POST /login, where is the user redirected? - On later requests, which filter restores the authenticated user, and from where?
- What does Spring Boot's default
SecurityFilterChainconfigure? - What does
hasRole("USER")check, and what happens if the role is missing? - With
maximumSessions(1).maxSessionsPreventsLogin(true), what happens on a second login? Which bean is also needed, and why? - Explain
IF_REQUIRED,ALWAYS,NEVERandSTATELESS. - Give three drawbacks of form login.
Answers
- The server keeps the authentication state in a session.
- Form login;
/loginand/logout. - 30 minutes (Tomcat);
server.servlet.session.timeout. From the last request (inactivity). - The
spring-session-jdbcdependency (plusspring.session.jdbc.initialize-schema=alwaysto create tables):SPRING_SESSIONandSPRING_SESSION_ATTRIBUTES. - To the protected page they originally asked for, or to
/if they opened/logindirectly. SecurityContextHolderFilter, from theSecurityContextstored in the HTTP session.- All requests authenticated, with form login and basic authentication.
- The authority
ROLE_USERin the security context; 403 Forbidden. - It fails: "Maximum sessions of 1 for this principal exceeded".
HttpSessionEventPublisher, so ended sessions are removed from the registry. IF_REQUIRED: create only when needed (default).ALWAYS: always create.NEVER: never create, but use an existing one.STATELESS: no session at all.- CSRF and session hijacking; session management overhead in distributed systems; database or cache load for shared sessions.