Spring Boot Security — Session Fixation via migrateSession
Users see others' data — session IDs don't rotate.
- Spring Security is a framework for handling authentication (who you are) and authorization (what you can do) in Java apps
- It uses a chain of servlet filters (SecurityFilterChain) to intercept requests before they reach controllers
- Core components: AuthenticationManager, SecurityContextHolder, UserDetailsService, PasswordEncoder
- Default protections: CSRF, session fixation, clickjacking headers
- Performance: ~1-5ms overhead per request from filter chain processing
- Biggest mistake: Disabling CSRF globally without understanding the stateless API exception
Think of Spring Boot Security Basics as a powerful tool in your developer toolkit. Once you understand what it does and when to reach for it, everything clicks together. Imagine you are building a private club. You need a bouncer at the door to check IDs (Authentication) and a list inside that says who is allowed in the VIP lounge versus the regular bar (Authorization). Spring Security is that automated bouncer and guest list, sitting in front of your application to ensure only the right people get to the right places — without you having to manually check every single request.
Here is the part most tutorials skip: that bouncer does not just stand at the front door. It is stationed at every internal door too. Even after someone gets in, the system keeps checking whether they are allowed to move deeper into the building. That layered nature is what makes Spring Security genuinely useful rather than just a checkbox exercise.
Spring Boot Security Basics is a core pillar of any production-grade Spring Boot application. It was designed to solve a specific, recurring problem: the repetitive and error-prone nature of implementing security logic manually across every endpoint.
Instead of scattering if statements to check user roles throughout your business logic, Spring Security uses a Security Filter Chain. This chain intercepts incoming requests before they ever reach your controllers, ensuring that security concerns are decoupled from your application code. That decoupling matters more than people realize early on — it means you can change your security model without touching your business logic, and your controllers stay focused on what they were actually written to do.
By utilizing the DelegatingFilterProxy, Spring bridges the gap between the Servlet container's lifecycle and Spring's ApplicationContext, allowing your security filters to be managed as standard Spring Beans. This article focuses on the modern Spring Security 6+ component-based configuration, deliberately stepping away from the deprecated WebSecurityConfigurerAdapter pattern. If you are still on that path, migrating is not optional — it is overdue.
What Is Spring Boot Security Basics and Why Does It Exist?
Spring Boot Security Basics exists because security is a cross-cutting concern — and cross-cutting concerns handled inline become unmanageable fast. Every time a developer writes an if (user.hasRole("ADMIN")) check inside a service method, they are making a bet that every other developer who touches that code will remember to replicate the same check elsewhere. That bet does not age well.
Spring Security's answer is the Security Filter Chain. Before any request reaches your @RestController or @Controller, it passes through an ordered series of filters. Each filter has a single responsibility: validate the CSRF token, extract and authenticate a bearer token, check session validity, enforce authorization rules. If a filter rejects the request, the chain short-circuits and your application code never executes.
This architecture gives you something genuinely valuable: your controllers can assume the request is legitimate. You do not write defensive role checks in service methods. You declare your security rules in one place, and the framework enforces them everywhere.
The DelegatingFilterProxy is the seam between the Servlet container world and the Spring world. The container knows nothing about Spring beans — it just knows about javax.servlet.Filter. DelegatingFilterProxy registers itself with the container as a standard filter, then delegates all actual work to FilterChainProxy, a Spring-managed bean that runs your configured SecurityFilterChain beans. That indirection is what makes Spring Security filters injectable, testable, and composable like any other Spring component.
- Each filter has one job — CSRF validation, session management, bearer token extraction, authorization — and does not bleed into adjacent concerns
- Filters execute in a strict, documented order; misplacing a custom filter breaks the contract and can silently open security gaps
- The chain short-circuits on rejection — a request blocked by filter 3 never reaches filter 4 or your controller
- Default filter ordering is deliberately conservative; understand what you are moving before you reorder anything
- Your controllers only see requests that passed every checkpoint — that contract is the foundation of the entire model
Spring Security Filter Chain — Order and Purpose of Built-in Filters
Spring Security's filter chain is not a random pile of filters — it is a carefully ordered pipeline where each filter has a single responsibility and executes in a strict sequence defined by FilterOrderRegistration. Understanding this order is not academic trivia; it determines whether your custom JWT filter runs before or after the session management filter, and whether your authorization rules are applied to the right set of requests.
The following diagram shows the default ordering of the key built-in filters in a typical SecurityFilterChain:
Filter Order (first to last): 1. DisableEncodeUrlFilter — prevent session ID in URLs (defense against session fixation via URL rewriting) 2. ForceEagerSessionCreationFilter — force session creation before authentication (used in some session-fixation scenarios) 3. ChannelProcessingFilter — enforce HTTPS redirect 4. WebAsyncManagerIntegrationFilter — integrate SecurityContext with async request processing 5. SecurityContextHolderFilter (or SecurityContextPersistenceFilter) — load/save SecurityContext from session 6. HeaderWriterFilter — add security headers (X-Content-Type-Options, X-Frame-Options, etc.) 7. CorsFilter — handle CORS preflight 8. CsrfFilter — validate CSRF token for state-changing requests 9. LogoutFilter — handle logout requests 10. OAuth2AuthorizationRequestRedirectFilter / OAuth2LoginAuthenticationFilter — OAuth2 login flows 11. UsernamePasswordAuthenticationFilter — form login authentication 12. DefaultLoginPageGeneratingFilter — generate login page if no custom page configured 13. DefaultLogoutPageGeneratingFilter — generate logout page 14. BasicAuthenticationFilter — HTTP Basic auth 15. RequestCacheAwareFilter — replay original request after authentication 16. SecurityContextHolderAwareRequestWrapper — wrap request to provide isUserInRole(), getUserPrincipal(), etc. 17. RememberMeAuthenticationFilter — handle remember-me cookies 18. AnonymousAuthenticationFilter — populate SecurityContext with anonymous token if no authentication present 19. SessionManagementFilter — detect session-fixation attacks, enforce concurrent session control 20. ExceptionTranslationFilter — translate security exceptions to HTTP responses (401/403) 21. AuthorizationFilter (formerly FilterSecurityInterceptor) — enforce access rules based on authorizeHttpRequests()
This order is by design — authentication filters (UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter) run before the AuthorizationFilter so that the security context is populated before access decisions are made. Similarly, CsrfFilter runs before authentication filters because CSRF tokens are validated regardless of whether the user is authenticated.
When you add a custom filter, you must tell Spring where in this order it should be inserted. The most common insertion points are: - addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class) — for JWT filters that need to run before form login - addFilterAfter(customFilter, BasicAuthenticationFilter.class) — for post-authentication processing - addFilterAt(customFilter, UsernamePasswordAuthenticationFilter.class) — replace a filter at that position (rare)
Misplacing a filter can have subtle consequences. For example, if you insert a JWT filter after ExceptionTranslationFilter, the AuthenticationEntryPoint (which redirects to login page) may override your 401 response.
org.springframework.security.web.FilterChainProxy. The logs will print the full chain in order when each request arrives. This is invaluable when debugging a custom filter that seems to be ignored or running at the wrong time.ExceptionTranslationFilter, any exception thrown by your filter will bypass the AuthenticationEntryPoint, potentially returning a 500 instead of a 401.springSecurity() mock MVC configurator runs the real filter chain — trust it over a unit test that only tests the filter logic in isolation.Authentication vs Authorization: Understanding the Difference
Authentication and authorization are the two pillars of security, but they are frequently confused — even in production code. Authentication answers who you are (identity verification), while authorization answers what you can do (access permission).
The following comparison table clarifies the distinction and shows how each concept maps to Spring Security components:
| Aspect | Authentication | Authorization |
|---|---|---|
| Purpose | Verify the identity of a user or system | Determine what the authenticated identity is allowed to access |
| Answer | Who are you? | What are you allowed to do? |
| Spring Security Component | AuthenticationManager, AuthenticationProvider, UserDetailsService, PasswordEncoder | AccessDecisionManager, SecurityExpressionHandler, @PreAuthorize, .hasRole() |
| Storage | Credentials (passwords, tokens, certificates) | Granted authorities (roles, permissions) |
| When it happens | First, during login | After authentication, on each request |
| HTTP Status | 401 Unauthorized (if failed) | 403 Forbidden (if insufficient) |
| Example | "I am Alice, here is my password." | "Alice is allowed to DELETE /orders." |
In Spring Security, authentication is typically handled by filters like UsernamePasswordAuthenticationFilter or BasicAuthenticationFilter. They extract credentials, delegate to an AuthenticationProvider, and on success store an Authentication object in SecurityContextHolder. That object contains the principal (user identity) and the GrantedAuthority list.
Authorization is enforced later in the filter chain by AuthorizationFilter. It evaluates the request against the rules declared in authorizeHttpRequests() (URL-based) or via method-level annotations like @PreAuthorize (method-based).
The critical thing to understand: authorization never happens before authentication. If a request reaches the AuthorizationFilter without a valid Authentication in the SecurityContext, it will be rejected with a 401, not a 403. This is why the filter order matters — authentication filters must run before the authorization filter.
A common mistake is to confuse the two at the code level. Developers sometimes write hasAuthority('ROLE_USER') inside an AuthenticationProvider check, which conflates credential validation with role assignment. Keep authentication and authorization logic in separate layers.
org.springframework.security and look for the filter that rejects the request. If the log shows UsernamePasswordAuthenticationFilter rejecting, it's an authentication problem. If it shows AuthorizationFilter rejecting, it's an authorization problem. This single piece of information can save hours of guesswork.Authentication Methods Comparison: Form Login, HTTP Basic, JWT, OAuth2
Choosing the right authentication method for your Spring Boot application depends on your architecture, client type, and security requirements. Each method has different trade-offs in terms of session management, statefulness, and use-case fit.
The table below compares the four most common approaches:
| Method | Session/Stateless | When to Use | Spring Security Configuration | Pros | Cons |
|---|---|---|---|---|---|
| Form Login | Session-based (stateful) | Traditional server-rendered web apps where users submit credentials via a form | .formLogin() with optional custom login page | - Simple to set up | |
| HTTP Basic | Stateless (credentials sent with every request) | Simple internal APIs, monitoring endpoints, or prototyping | .httpBasic() | - Very simple to implement | |
| JWT (Bearer Token) | Stateless (token in Authorization header) | REST APIs, single-page applications, mobile apps | Custom filter before UsernamePasswordAuthenticationFilter | - Stateless, scalable | |
| OAuth2 / OpenID Connect | Delegated (stateless or session-based depending on grant type) | Applications that need to authenticate via external providers (Google, GitHub, etc.) or enable single sign-on | .oauth2 for social login; .oauth2 for API protection | - Delegates credential handling to trusted providers |
The choice often comes down to this: if you control both the client and server (e.g., a full-stack web app), form login is straightforward and battle-tested. If you are building a JSON API consumed by a SPA or mobile app, JWT with a stateless session policy is the standard. If you need to integrate with Google, GitHub, or enterprise SAML, OAuth2 is the only sane option.
A common anti-pattern is mixing stateful and stateless approaches without careful segregation. For example, having a session-based form login for the admin panel and JWT for the public API works — but you must ensure the filter chains are properly scoped so that session cookies don't interfere with token-based requests.
SecurityContext for each request — it just doesn't persist it across requests. The context is set by the authentication filter (e.g., JWT filter) and discarded after the response is sent. There is no HttpSession used. This is ideal for REST APIs but means you cannot rely on session-based features like Remember-Me.Advantages and Disadvantages of Spring Boot Security
Spring Security is the most widely used security framework for Java applications, but it's not a silver bullet. Understanding its strengths and weaknesses helps you make informed architectural decisions and avoid common pitfalls.
Advantages:
| Advantage | Explanation |
|---|---|
| Battle-tested defaults | Secure defaults for CSRF, session fixation, clickjacking, and headers — you get protection against OWASP Top 10 without configuration. |
| Centralized configuration | All security rules in one place (SecurityFilterChain beans). No scattered role checks in business logic. |
| Comprehensive authentication providers | Supports form login, HTTP Basic, Digest, LDAP, OAuth2, SAML, JWT, and custom providers via AuthenticationProvider interface. |
| Fine-grained authorization | URL-based (authorizeHttpRequests) and method-level (@PreAuthorize, @Secured, @PostAuthorize) access control. |
| Testability | @WithMockUser, @WithAnonymousUser, and MockMvc integration make security testing straightforward. |
| Production incident patterns | Widely deployed — many edge cases (BCrypt truncation, thread-local propagation, session replication) are documented. |
Disadvantages:
| Disadvantage | Explanation |
|---|---|
| Steep learning curve | The filter chain, multiple configuration styles, and deprecation cycles (e.g., WebSecurityConfigurerAdapter removal) confuse newcomers. |
| Configuration complexity | Real-world setups often require multiple SecurityFilterChain beans, custom filters, and careful ordering — easy to misconfigure. |
| Performance overhead | Filter chain adds 1-5ms per request; BCrypt at strength 12 adds ~250ms on login. Not negligible for high-throughput systems. |
| Over-engineering trap | It's tempting to add OAuth2 or complex JWT infrastructure when simple HTTP Basic would suffice for internal tools. |
| XML/annotation legacy | Older configuration styles (XML, pre-lambda DSL) are still in many codebases, creating migration burden. |
| Async context handling | SecurityContextHolder defaults to ThreadLocal, which breaks in async scenarios unless you explicitly propagate context. |
The key takeaway: Spring Security is excellent at what it does — providing a comprehensive, extensible security framework — but it expects you to understand its architecture. The disadvantages primarily stem from misuse or underestimating the learning curve.
For most Spring Boot applications, the advantages far outweigh the disadvantages, especially once you invest the time to understand the filter chain model. The alternative — rolling your own security — almost always results in worse security and higher maintenance cost.
5 Practice Projects to Master Spring Security
Theory is necessary, but building real projects is how you internalize Spring Security's concepts. These five practice projects are ordered from beginner to advanced and cover the most common production scenarios.
1. Role-Based Access Control (RBAC) with Multiple User Types Build a web application with three user roles: USER, MODERATOR, and ADMIN. Implement endpoints that are accessible only to specific roles. Use an in-memory or database-backed UserDetailsService with BCryptPasswordEncoder. Configure authorizeHttpRequests() to enforce role-based access. What you'll learn: Request matcher ordering, role hierarchy, and the difference between hasRole() and hasAuthority(). Production relevance: Most enterprise applications require role-based permissions.
2. Method Security with @PreAuthorize Extend the RBAC project by adding method-level security using @EnableMethodSecurity and @PreAuthorize. For example, a bank transfer method should only be callable by users with AUTHORIZED_SIGNER permission. What you'll learn: How method security works with AOP, SpEL expressions, and how it complements URL-based security. Production relevance: Method security is essential for fine-grained access control in service layers.
3. Multi-Factor Authentication (MFA) with Time-Based One-Time Password (TOTP) Add a second factor to a Spring Security form login. After password authentication, redirect the user to an MFA challenge page that expects a 6-digit TOTP code (like Google Authenticator). Use a custom AuthenticationProvider that verifies both the password and the TOTP. What you'll learn: How to chain authentication providers, store and validate TOTP secrets, and handle partial authentication states. Production relevance: MFA is becoming mandatory for sensitive applications.
4. Stateless REST API with JWT and Refresh Tokens Build a REST API that authenticates with username/password and returns a short-lived access token (JWT) plus a long-lived refresh token. Implement a custom filter that validates the JWT on every request. Use the refresh token endpoint to issue new access tokens. What you'll learn: Custom filter placement, token generation and validation, stateless session policy, and refresh token rotation. Production relevance: This is the foundational pattern for most modern APIs.
5. OAuth2 Social Login with Google/GitHub and Custom User Registration Configure Spring Security's OAuth2 client to allow login via Google or GitHub. After a successful OAuth2 authentication, capture the user's email and store them in a local database, possibly prompting for additional details on first login. Use @AuthenticationPrincipal to retrieve the user in controllers. What you'll learn: OAuth2 login flow, OAuth2UserService customization, mapping external principals to local users, and handling multiple authentication providers (OAuth2 + form login). Production relevance: Social login is ubiquitous in consumer-facing applications.
Common Mistakes and How to Avoid Them
Most Spring Security issues in production trace back to one of five mistakes. Understanding them up front saves a disproportionate amount of debugging time later.
1. Disabling CSRF globally without understanding what it protects. CSRF protection exists specifically for session-cookie-based authentication. If your application uses JWTs or OAuth2 tokens sent in the Authorization header, CSRF is genuinely unnecessary for those endpoints — tokens provide request authenticity that cookies cannot. The mistake is applying .csrf(AbstractHttpConfigurer::disable) to an application that also uses session cookies somewhere, usually for an admin panel or a legacy flow that did not get migrated. The safe approach is to scope the disable to specific request matchers using csrf.ignoringRequestMatchers("/api/**") and leave protection active everywhere else.
2. Misordering request matchers. Spring Security evaluates matchers in the order they are declared and stops at the first match. A rule like anyRequest().authenticated() placed before .requestMatchers("/api/v1/public/**").permitAll() means the broader rule matches every request first and the permit-all rule never fires. The fix is simple but requires discipline: always order from most specific to least specific, and add a comment explaining why the order is intentional.
3. Not encoding passwords — or encoding them inconsistently. This is more subtle than it sounds. The failure mode is not always an error at startup — sometimes it is a silent mismatch at login time that sends every user to the login page with no explanation. The root cause is usually a PasswordEncoder bean that was defined in one configuration class but a different hardcoded encoder being used in a migration script or a test fixture. Treat your PasswordEncoder bean as a singleton used in exactly one place for encoding and one place for verification.
4. Using WebSecurityConfigurerAdapter in Spring Security 6+. This class was deprecated in Spring Security 5.7 and removed entirely in 6.0. If you are on Spring Boot 3.x and still extending it, your application will not compile. The migration path is to declare SecurityFilterChain beans instead of overriding configure(HttpSecurity http). The resulting configuration is more readable and easier to test.
5. Not handling authentication and authorization exceptions explicitly. The default behavior when security blocks a request is to redirect to a login page or return a generic HTML error page. For a REST API, that is wrong on both counts. Clients expecting JSON get HTML, and error pages often include stack traces or framework version information. Define a custom AuthenticationEntryPoint that returns a structured 401 JSON body, and a custom AccessDeniedHandler that returns a structured 403 JSON body. This takes twenty minutes and prevents an entire category of information disclosure.
passwordEncoder.matches() for verification, never string equality. The matches() method uses constant-time comparison internally, which prevents timing attacks that could leak whether a credential is partially correct.csrf().disable() scoped to the API path matcher, leave it enabled for the web paths. Do not use ignoringRequestMatchers() on a single chain unless you understand every path included in that pattern.Session Fixation Attack in Production
SessionFixationProtectionStrategy with migrateSession(), which does not invalidate the old session ID in clustered environments using async session replication. Under peak load, the replication lag created a race condition where two users briefly shared a session window. This was not caught in staging because staging ran a single node.changeSessionId() strategy and ensure session replication completes before the response is flushed to the client: sessionManagement(session -> session.sessionFixation().changeSessionId()). In addition, add session event listeners to log session creation and invalidation so this class of bug surfaces immediately in your observability stack rather than through user complaints.- Never override default security protections without reading the Javadoc for what the old behavior actually guaranteed
- Staging environments that do not replicate production topology will miss an entire class of distributed session bugs
- Test session ID rotation explicitly after login — automate it in your CI pipeline, not just in manual QA
- Monitor session ID changes after authentication in production logs; a missing rotation is an incident waiting to happen
authenticated() rule to shadow your permitAll() entries. Enable DEBUG logging on org.springframework.security to see which filter is rejecting the request.SecurityContextHolder.getContext().setAuthentication(authToken) AND that your filter is positioned before UsernamePasswordAuthenticationFilter in the chain. A correctly validated token that never populates the context is indistinguishable from no token at all..csrf(csrf -> csrf.disable()) for stateless APIs only. If the app uses session cookies anywhere, scope it with ignoringRequestMatchers() instead of a blanket disable.Key takeaways
HttpSecurity.authorizeRequests() being replaced by authorizeHttpRequests() — show up in minor releases and cause production incidents for teams that skip them.Common mistakes to avoid
5 patternsDisabling CSRF globally for all endpoints
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")). Leave protection active for any path that a browser session touches.Misordering request matchers in authorizeHttpRequests
/api/v1/public/ must appear before /api/v1/, which must appear before anyRequest(). Add a comment in the configuration explaining the required ordering — future maintainers will thank you.Using deprecated WebSecurityConfigurerAdapter in Spring Security 6+
Storing passwords with MD5 or SHA-256 without per-password salting
Not handling authentication and authorization exceptions explicitly
Interview Questions on This Topic
What is the difference between Authentication (Who are you?) and Authorization (What can you do?) in the context of Spring Security?
Authentication object stored in SecurityContextHolder. That object carries the principal (usually a UserDetails instance) and the granted authorities. Authorization happens after authentication and answers whether the verified identity has permission to perform the requested action. The SecurityFilterChain enforces both: UsernamePasswordAuthenticationFilter handles authentication; AuthorizationFilter (formerly FilterSecurityInterceptor) handles authorization. The ordering is non-negotiable — you cannot authorize an identity you have not verified.Frequently Asked Questions
That's Spring Boot. Mark it forged?
13 min read · try the examples if you haven't