Appearance
Spring Boot Security (Part 3) | Form Based Authentication and Stateful Sessions
The Hotel Keycard and Front Desk Analogy
Imagine checking into a luxury resort hotel. When you arrive in the lobby, you do not present a credit card and passport to every elevator operator, room service waiter, and gym attendant you encounter during your stay. You visit the front desk once. You present your passport, sign the guest registry, and provide payment.
In exchange, the desk clerk hands you a magnetized plastic keycard encoded with an arbitrary session key. The hotel's central server links that keycard code to your guest record in memory. When you tap into your room or enter the executive lounge, the electronic door scanner simply reads the keycard code and checks the central computer to confirm your privileges. When you check out on Sunday, the clerk deactivates your keycard in the system, and your access terminates immediately.
This is the exact model behind Stateful Form Based Authentication. When a user logs in via an HTML login form, the server verifies their credentials against the database, allocates an in memory HttpSession on the server, and transmits a random session identifier (the JSESSIONID cookie) to the user's browser. The browser attaches that cookie to all subsequent requests. The server inspects the session store to identify the user on every page load.
This lecture covers stateful authentication architecture, Spring Security formLogin configuration, custom login pages, UserDetailsService and AuthenticationManager internals, roles versus authorities, and session lifecycle management.
Stateful Authentication Architecture
[ Client Browser ] [ Spring Boot Server ]
| |
| --- 1. POST /login (username, password) -------------> |
| | Authenticates via UserDetailsService
| | Creates in-memory HttpSession
| | Generates JSESSIONID = "A189DF2"
| <-- 2. Set-Cookie: JSESSIONID=A189DF2 (HTTP 302) ------ |
| |
| --- 3. GET /orders (Cookie: JSESSIONID=A189DF2) -----> |
| | Looks up Session "A189DF2" in memory
| | Resolves SecurityContext & Authorities
| <-- 4. HTTP 200 OK (Orders HTML/JSON) ----------------- |Key Characteristics:
- Server Side State: The server must retain active sessions in memory (heap memory or an external store like Redis).
- Automatic Browser Management: Web browsers store the cookie securely and transmit it automatically on every subsequent HTTP request.
- Instant Revocation: If an administrator invalidates a session on the server, the user is logged out immediately on their next click.
Configuring Form Based Authentication in Spring Security 6
In modern Spring Boot 3, security is configured via component based SecurityFilterChain beans:
java
package com.example.orderservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/", "/home", "/css/**", "/js/**", "/login").permitAll()
// Role-restricted endpoints
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
// All other endpoints require authentication
.anyRequest().authenticated()
)
.formLogin(form -> form
// Specify custom login page route
.loginPage("/login")
// The URL where the HTML form posts credentials
.loginProcessingUrl("/perform_login")
// Where to redirect after successful authentication
.defaultSuccessUrl("/dashboard", true)
// Where to redirect on failed credentials
.failureUrl("/login?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/perform_logout")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.logoutSuccessUrl("/login?logout=true")
.permitAll()
);
return http.build();
}
}The Internal Authentication Pipeline
What happens under the hood when a user submits their login form?
[ POST /perform_login ]
|
v
[ UsernamePasswordAuthenticationFilter ]
- Extracts username and password from request parameters
- Constructs UsernamePasswordAuthenticationToken(user, pass)
|
v
[ AuthenticationManager (ProviderManager) ]
- Loops through registered AuthenticationProviders
|
v
[ DaoAuthenticationProvider ]
- Calls userDetailsService.loadUserByUsername(username)
- Retrieves UserDetails from database (hashed password + roles)
- Delegates to PasswordEncoder.matches(rawPassword, hashedPassword)
|
+-------+-------+
| |
[ Success ] [ Failure ] -> Throws BadCredentialsException
|
v
- Creates fully populated Authentication object (with GrantedAuthorities)
- Saves into SecurityContext: SecurityContextHolder.getContext().setAuthentication(auth)
- Binds SecurityContext into active HttpSessionUserDetailsService: Loading Users from the Database
To authenticate real database users, implement UserDetailsService:
java
package com.example.orderservice.service;
import com.example.orderservice.entity.UserEntity;
import com.example.orderservice.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity entity = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
// Note: Spring Security expects roles to begin with "ROLE_"
String roleWithPrefix = "ROLE_" + entity.getRole();
return new org.springframework.security.core.userdetails.User(
entity.getUsername(),
entity.getPasswordHash(),
entity.isEnabled(),
true, // accountNonExpired
true, // credentialsNonExpired
true, // accountNonLocked
Collections.singletonList(new SimpleGrantedAuthority(roleWithPrefix))
);
}
}Password Encoding with BCrypt
Never store plain text passwords. Register a BCryptPasswordEncoder:
java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}Roles vs Authorities in Spring Security
A common interview topic is the semantic difference between a Role and an Authority:
| Dimension | Authority (hasAuthority) | Role (hasRole) |
|---|---|---|
| Granularity | Fine grained individual permission (e.g. OP_DELETE_ORDER, CAN_EXPORT_PDF) | Coarse grained group of permissions (e.g. ADMIN, CUSTOMER) |
| Storage Format | Arbitrary string: READ_PRIVILEGE | Stored in database as ROLE_ADMIN or prefixed automatically |
| Method Check | .hasAuthority("OP_DELETE") | .hasRole("ADMIN") (Spring automatically prefixes ROLE_) |
When you configure .hasRole("ADMIN"), Spring Security automatically prepends ROLE_ and checks for the authority ROLE_ADMIN.
Interview Questions & Pitfalls
Q1: What is the difference between stateful form based authentication and stateless token authentication?
Stateful form based authentication maintains active user sessions in server side memory (HttpSession) and identifies the client using a browser session cookie (JSESSIONID). Stateless token authentication (such as JWT) stores all identity and permission claims inside a cryptographically signed token held exclusively by the client, requiring zero session memory on the server.
Q2: What is the purpose of SecurityContextHolder in Spring Security?
SecurityContextHolder is the central storage location where Spring Security stores details of the currently authenticated principal. It uses a ThreadLocal strategy by default, allowing any service or component on that execution thread to inspect the active user identity (SecurityContextHolder.getContext().getAuthentication()).
Q3: Why does .hasRole("ADMIN") check for ROLE_ADMIN under the hood?
Spring Security enforces a standardized prefix convention: roles are treated as special GrantedAuthority objects that must begin with the prefix ROLE_. Calling hasRole("ADMIN") is syntactic sugar for hasAuthority("ROLE_ADMIN").
Q4: How does Spring Security prevent session fixation attacks in form login?
By default, Spring Security employs session fixation protection (via changeSessionId() or newSession()). When a user authenticates successfully, Spring invalidates the temporary pre authentication session ID and issues a brand new session ID, preventing attackers from hijacking sessions using pre set cookie values.
Q5: What is the role of DaoAuthenticationProvider in the authentication flow?
DaoAuthenticationProvider is Spring's default AuthenticationProvider implementation for database authentication. It coordinates between UserDetailsService (which fetches user credentials by username) and PasswordEncoder (which verifies whether the submitted raw password matches the stored cryptographic hash).