Senior 13 min · March 09, 2026

Spring Boot Security — Session Fixation via migrateSession

Users see others' data — session IDs don't rotate.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

SecurityConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package io.thecodeforge.security;

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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * io.thecodeforge: Standard Filter Chain Configuration
     *
     * Rule ordering is intentional and matters:
     *   1. Most specific path matchers come first.
     *   2. Broader matchers (anyRequest) always come last.
     *
     * Swapping that order causes Spring Security to apply the
     * broader rule first, silently locking out public endpoints.
     *
     * CSRF is disabled here because this config targets a
     * stateless REST API using token-based auth. Do NOT
     * disable CSRF for session-based web applications.
     */
    @Bean
    public SecurityFilterChain forgeSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            // Stateless REST API: CSRF tokens are not needed because
            // we do not rely on browser session cookies for auth.
            .csrf(AbstractHttpConfigurer::disable)

            .authorizeHttpRequests(auth -> auth
                // Public endpoints — no authentication required
                .requestMatchers("/api/v1/public/**").permitAll()
                // Health and readiness probes should be accessible to infra
                .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                // Admin endpoints require the ADMIN role (stored as ROLE_ADMIN)
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                // Standard user endpoints — accessible to USER or ADMIN
                .requestMatchers("/api/v1/user/**").hasAnyRole("USER", "ADMIN")
                // Everything else must be authenticated; explicit is safer than implicit
                .anyRequest().authenticated()
            )

            // No server-side session state — tokens carry identity per request
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // HTTP Basic for simplicity; swap for JWT filter in production
            .httpBasic(withDefaults());

        return http.build();
    }
}
The Filter Chain Mental Model
  • 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
Production Insight
The filter chain executes for every request, including static resources, actuator endpoints, and OPTIONS preflight calls from CORS.
A misconfigured permitAll() pattern is the most common cause of mysterious 403 errors on actuator health endpoints in Kubernetes readiness probes — the probe fails, the pod restarts, and the team spends an hour ruling out application bugs before looking at security config.
Always validate your security configuration against a complete request path matrix before shipping to production. That matrix should include: unauthenticated GET, unauthenticated POST, authenticated with insufficient role, authenticated with correct role, and OPTIONS preflight for any CORS-enabled endpoint.
Key Takeaway
Centralizing security in a filter chain prevents scattered authorization logic from creeping into service and controller layers.
The filter chain is not just a convenience — it is a contract. Your application code is supposed to assume the request is legitimate. Break that assumption by mixing security checks into business logic and you will spend years untangling the two.
The filter chain enforces security at the infrastructure layer so your application layer stays focused on the problem it was written to solve.
When to Customize the Filter Chain
IfStateless REST API with JWT
UseDisable CSRF, apply STATELESS session policy, insert your JWT extraction filter before UsernamePasswordAuthenticationFilter, and return 401 from a custom AuthenticationEntryPoint instead of redirecting to a login page
IfTraditional web app with server-rendered forms
UseKeep CSRF enabled, use STATEFUL sessions, configure formLogin() with a custom login page, and define a logout handler that invalidates the session and clears the cookie
IfMixed application serving both API consumers and browser users
UseDeclare two separate SecurityFilterChain beans — one scoped to /api/ with CSRF disabled and STATELESS policy, one scoped to / for the web UI with full CSRF and session support. Use @Order to control which chain evaluates first.

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.

CustomFilterPlacementConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.security;

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;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class CustomFilterPlacementConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Custom JWT filter runs BEFORE UsernamePasswordAuthenticationFilter
            .addFilterBefore(new JwtAuthenticationFilter(),
                             UsernamePasswordAuthenticationFilter.class)

            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic();

        return http.build();
    }
}
Filter Order Discovery
If you ever need to inspect the exact filter order at runtime, enable DEBUG logging for 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.
Production Insight
Filter ordering is not a development-time concern — it is a runtime contract. A filter placed incorrectly can cause intermittent failures that only surface under specific request patterns. For example, if you accidentally place a filter after ExceptionTranslationFilter, any exception thrown by your filter will bypass the AuthenticationEntryPoint, potentially returning a 500 instead of a 401.
Always test filter ordering with a request that exercises the full chain, including both success and error paths. The Spring Security Test module's 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:

AspectAuthenticationAuthorization
PurposeVerify the identity of a user or systemDetermine what the authenticated identity is allowed to access
AnswerWho are you?What are you allowed to do?
Spring Security ComponentAuthenticationManager, AuthenticationProvider, UserDetailsService, PasswordEncoderAccessDecisionManager, SecurityExpressionHandler, @PreAuthorize, .hasRole()
StorageCredentials (passwords, tokens, certificates)Granted authorities (roles, permissions)
When it happensFirst, during loginAfter authentication, on each request
HTTP Status401 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.

AuthenticationAuthorizationExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.thecodeforge.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

    @GetMapping("/whoami")
    public String whoAmI() {
        // Authentication object from SecurityContext
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            return "Not authenticated";
        }
        // principal provides identity (authentication)
        String username = auth.getName();
        // authorities define what user can do (authorization)
        java.util.Collection<?> authorities = auth.getAuthorities();
        return "Authenticated as: " + username + ", authorities: " + authorities;
    }
}
The 401 vs 403 Trap
Many developers conflate 401 and 403 when building REST APIs. Remember: a 401 Unauthorized means "I don't know who you are" — authentication failed or is missing. A 403 Forbidden means "I know who you are, but you're not allowed to do that" — authentication succeeded but authorization failed. Sending the wrong status code can confuse clients and leak information. For example, returning 403 for an unauthenticated user suggests that the user exists and is known, which is an information disclosure.
Production Insight
When debugging security issues in production, the first thing to check is whether the request is failing at authentication or authorization. Enable DEBUG logging for 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.

MethodSession/StatelessWhen to UseSpring Security ConfigurationProsCons
Form LoginSession-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 BasicStateless (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 appsCustom filter before UsernamePasswordAuthenticationFilter- Stateless, scalable
OAuth2 / OpenID ConnectDelegated (stateless or session-based depending on grant type)Applications that need to authenticate via external providers (Google, GitHub, etc.) or enable single sign-on.oauth2Login() for social login; .oauth2ResourceServer() 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.

MultipleAuthConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package io.thecodeforge.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class MultipleAuthConfig {

    // Chain for public API endpoints — stateless, JWT
    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(new JwtAuthenticationFilter(),
                             UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        return http.build();
    }

    // Chain for admin web UI — session-based, form login
    @Bean
    @Order(2)
    public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/admin/**")
            .csrf(csrf -> csrf.ignoringRequestMatchers("/admin/api/**"))  // partial stateless
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
            .formLogin(withDefaults())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/public/**").permitAll()
                .anyRequest().hasRole("ADMIN")
            );
        return http.build();
    }
}
Stateless Doesn't Mean No Sessions
Even with a stateless session policy (STATELESS), Spring Security will still create a 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.
Production Insight
When choosing an authentication method, consider the operational overhead. HTTP Basic is trivial but exposes credentials on every request (even over HTTPS, they are in the request header). JWT avoids sessions but introduces token length (can be 1-2KB per request) and revocation challenges. OAuth2 provides the best security model but adds network calls to the identity provider on every token validation (unless you use token introspection caching or JWKs with local validation). Measure the latency impact in your specific environment before committing to a method.

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:

AdvantageExplanation
Battle-tested defaultsSecure defaults for CSRF, session fixation, clickjacking, and headers — you get protection against OWASP Top 10 without configuration.
Centralized configurationAll security rules in one place (SecurityFilterChain beans). No scattered role checks in business logic.
Comprehensive authentication providersSupports form login, HTTP Basic, Digest, LDAP, OAuth2, SAML, JWT, and custom providers via AuthenticationProvider interface.
Fine-grained authorizationURL-based (authorizeHttpRequests) and method-level (@PreAuthorize, @Secured, @PostAuthorize) access control.
Testability@WithMockUser, @WithAnonymousUser, and MockMvc integration make security testing straightforward.
Production incident patternsWidely deployed — many edge cases (BCrypt truncation, thread-local propagation, session replication) are documented.

Disadvantages:

DisadvantageExplanation
Steep learning curveThe filter chain, multiple configuration styles, and deprecation cycles (e.g., WebSecurityConfigurerAdapter removal) confuse newcomers.
Configuration complexityReal-world setups often require multiple SecurityFilterChain beans, custom filters, and careful ordering — easy to misconfigure.
Performance overheadFilter chain adds 1-5ms per request; BCrypt at strength 12 adds ~250ms on login. Not negligible for high-throughput systems.
Over-engineering trapIt's tempting to add OAuth2 or complex JWT infrastructure when simple HTTP Basic would suffice for internal tools.
XML/annotation legacyOlder configuration styles (XML, pre-lambda DSL) are still in many codebases, creating migration burden.
Async context handlingSecurityContextHolder 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.

When Spring Security Is Overkill
For very simple internal services that are only accessible within a private network (e.g., a microservice that talks only to other services via mutual TLS), full Spring Security may add unnecessary complexity. In such cases, consider relying on network-level security (firewalls, VPNs, mTLS) rather than application-level security. But the moment the service is exposed to a browser or an untrusted network, Spring Security is the right choice.
Production Insight
The biggest disadvantage in production is not a framework flaw but the tendency to override defaults without understanding the consequences. Every time you disable CSRF globally, or change the session fixation strategy, or set a permissive CORS policy, you are accepting a risk. Document those decisions with a comment explaining why the default is inappropriate for your specific use case. That documentation will save the next engineer — possibly you — from hours of debugging.

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.

PreAuthorizeMethodSecurityExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package io.thecodeforge.service;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class TransferService {

    /**
     * Only users with AUTHORIZED_SIGNER permission can execute transfers.
     * The method will throw AccessDeniedException if the caller lacks this authority.
     */
    @PreAuthorize("hasPermission(#accountId, 'TRANSFER')")
    public void transferFunds(Long accountId, double amount) {
        // business logic
    }
}
Project Progression
Start with project 1 (RBAC) and project 4 (JWT API) — they cover 80% of real-world use cases. Once you have those solid, move to MFA and OAuth2. Method security (project 2) is best learned after you understand URL-based authorization, as it complements rather than replaces it.
Production Insight
Each of these projects directly translates to a real production scenario. When hiring, asking a candidate to walk through the design of a JWT-based stateless API or an RBAC system tells you more about their Spring Security competence than any theoretical question. Build these projects, deploy them to a cloud platform (even a free tier), and test them with actual HTTP clients — the experience of debugging a misconfigured CORS policy or a missing filter order at 2 AM is unforgettable.

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.

PasswordEncoderConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package io.thecodeforge.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {

    /**
     * io.thecodeforge: Password encoding strategy.
     *
     * WHY BCrypt:
     *   - Adaptive: work factor increases as hardware improves
     *   - Built-in per-password salt: no rainbow table attacks
     *   - Intentionally slow: ~250ms at strength 12 on modern hardware
     *
     * WHY NOT MD5 or SHA-256:
     *   - They are fast — GPUs can compute billions per second
     *   - Fast hashing is a liability for passwords, not a feature
     *   - Neither includes a salt by design
     *
     * STRENGTH FACTOR GUIDANCE:
     *   - 10: ~100ms — acceptable minimum for most apps
     *   - 12: ~250ms — recommended for login endpoints
     *   - 14: ~1000ms — appropriate only if login volume is very low
     *
     * IMPORTANT: BCrypt silently truncates input beyond 72 bytes.
     * Validate password length at the controller/service layer
     * before this encoder ever sees the value.
     */
    @Bean
    public PasswordEncoder forgePasswordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}
Production Security Warning
The most expensive mistake with Spring Boot Security Basics is over-engineering early. Reaching for OAuth2 Authorization Server or full JWT infrastructure when a simple in-memory user store would get you through the prototype stage adds weeks of complexity for zero security benefit at that scale. Conversely, carrying that in-memory store into production because it was convenient during development is how you end up with credentials baked into application.properties and committed to source control. The right question is not 'what is the most secure option?' — it is 'what threat model am I actually defending against at this stage, and what is proportionate to that threat?'
Production Insight
BCrypt at strength 12 takes roughly 250ms per operation on modern server hardware. That is your login endpoint's minimum response time floor — factor it into your SLA and make sure your load balancer timeout is not set lower than that.
Password length validation is not optional. BCrypt's 72-byte truncation means two passwords that differ only after byte 72 will produce the same hash. A user who sets a very long passphrase may later be locked out if your password change flow encodes the full string but your login flow truncates it inconsistently. Enforce a sensible maximum length — 128 characters is a reasonable ceiling — before the value reaches BCrypt.
Always use 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.
Key Takeaway
Understand the threat before you configure the defense. CSRF exists for a specific attack against a specific authentication mechanism — disable it only where that mechanism is absent.
Password hashing is not a detail. BCrypt with strength 10 to 12 is the current baseline. Anything weaker is a known liability. Anything you implement yourself is almost certainly weaker than BCrypt.
The common thread in most Spring Security production incidents is not a framework bug — it is a developer who changed a default without fully understanding what that default was preventing.
CSRF Decision Matrix
IfStateless API using JWT or OAuth2 bearer tokens in the Authorization header
UseDisable CSRF — the token in the header provides request authenticity that session cookies cannot. Browsers cannot set arbitrary headers cross-origin without a preflight, so the attack vector does not exist.
IfBrowser-based application using server-managed session cookies
UseKeep CSRF enabled — this is exactly the scenario it was designed for. A cookie is sent automatically by the browser on any cross-origin request, and CSRF tokens break that attack path.
IfApplication with both API endpoints and session-based web pages
UseUse two SecurityFilterChain beans. Apply 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.
● Production incidentPOST-MORTEMseverity: high

Session Fixation Attack in Production

Symptom
Users intermittently see other users' data; session IDs do not change after authentication completes.
Assumption
Spring Security's default session fixation protection is enabled and working correctly across all nodes.
Root cause
A custom session management configuration accidentally overwrote the default 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.
Fix
Revert to Spring Security's default 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.
Key lesson
  • 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
Production debug guideFrom 403 errors to authentication failures — a structured triage approach5 entries
Symptom · 01
403 Forbidden on all endpoints
Fix
Check if CSRF protection is enabled for non-browser clients sending JSON; verify that request matcher ordering does not allow a broad authenticated() rule to shadow your permitAll() entries. Enable DEBUG logging on org.springframework.security to see which filter is rejecting the request.
Symptom · 02
Authentication object is null in controller
Fix
Verify SecurityContextHolder strategy matches your threading model. MODE_THREADLOCAL works for synchronous servlets but loses context in async handlers — switch to MODE_INHERITABLETHREADLOCAL or propagate the context explicitly with DelegatingSecurityContextRunnable.
Symptom · 03
BCrypt encoded password does not match
Fix
Confirm the strength factor is consistent between encoding and verification. Also check for silent password truncation — BCrypt silently ignores bytes beyond position 72, which means a password like 'correcthorsebatterystaple123456789012345678901234567890123456789012345' hashes identically to its first 72 bytes. Validate input length before it reaches the encoder.
Symptom · 04
Custom UserDetailsService not called
Fix
Confirm the bean carries @Component or is declared as @Bean in a @Configuration class. If you have multiple AuthenticationProvider beans registered, check which one is winning — Spring picks the first provider that supports the token type, and a misconfigured DaoAuthenticationProvider that does not reference your service will bypass it entirely.
Symptom · 05
JWT filter runs but SecurityContext is still empty after validation
Fix
Ensure you are calling 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.
★ Spring Security Emergency DebuggingWhen security breaks in production, start here. Work top to bottom — do not skip steps.
All requests return 403
Immediate action
Check if CSRF is enabled for REST clients. This is the most common cause after a Spring Boot version bump changes defaults.
Commands
curl -v -H 'Content-Type: application/json' -X POST http://localhost:8080/api/test
grep -r 'csrf' src/main/java/**/SecurityConfig.java
Fix now
Add .csrf(csrf -> csrf.disable()) for stateless APIs only. If the app uses session cookies anywhere, scope it with ignoringRequestMatchers() instead of a blanket disable.
User authenticated but has no roles+
Immediate action
Check GrantedAuthority prefix. hasRole('ADMIN') internally prepends ROLE_ and looks for ROLE_ADMIN. If your data store saves ADMIN without the prefix, the check silently fails.
Commands
docker logs app-container | grep 'GrantedAuthority'
kubectl exec -it pod -- jcmd 1 VM.classloader_stats
Fix now
Ensure roles are prefixed with 'ROLE_' in your UserDetailsService, or switch to hasAuthority('ADMIN') and drop the prefix convention entirely — just be consistent across the codebase.
Security Implementation Comparison
AspectManual Security CheckSpring Boot Security
MaintenanceHard — security logic is spread across dozens of controllers and services; adding a new role requires touching multiple filesEasy — centralized in a single SecurityFilterChain bean; adding a new rule is a one-line change in one file
StandardizationAd-hoc — high risk of bypasses, missed endpoints, and edge-case errors that only surface under specific conditionsStandard — uses industry-proven patterns and ships secure defaults aligned with OWASP recommendations
Vulnerability ProtectionManual — every developer must independently implement CSRF validation, XSS headers, and clickjacking protectionBuilt-in — defaults protect against the most common OWASP Top 10 vectors without any additional configuration
FlexibilityLow — swapping from Basic Auth to JWT requires refactoring security logic out of every controller that embedded itHigh — authentication provider changes are localized to the SecurityFilterChain; controllers are unaffected
AuditabilityDifficult — tracing who can access what requires reading through every controller, service, and utility classClear — declarative security rules in one location make access policy readable by anyone who can open SecurityConfig.java
Performance ImpactVariable — depends entirely on implementation quality; inconsistent checks add unpredictable latencyPredictable — roughly 1 to 5ms overhead per request from filter chain processing, independent of application complexity
Testing ComplexityHigh — each controller's security logic must be tested independently; gaps are common because coverage is manualLow — configure once, test the configuration, and security applies globally; @WithMockUser handles most scenarios

Key takeaways

1
Spring Security is not optional infrastructure
it is a foundational layer that every production Spring Boot application needs, and understanding its architecture pays dividends every time something breaks in production.
2
The filter chain is the core mental model. Everything else
authentication providers, UserDetailsService, PasswordEncoder — feeds into or out of that chain. Once the chain clicks, the rest of the API makes sense.
3
Start simple and earn complexity. In-memory authentication with form login is a legitimate starting point. Reach for OAuth2 or JWT when you have a concrete reason
not because a tutorial used it.
4
Defaults are conservative by design. Before disabling any default protection, name the attack it prevents and confirm your application's architecture means that attack cannot apply to you.
5
The lambda DSL for HttpSecurity configuration is not cosmetic. It removes the ambiguity of method chaining with side effects and makes the security policy readable to someone who has never seen the class before. Use it consistently.
6
Read the release notes for every Spring Security minor version, not just majors. Behavioral changes to defaults
like 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 patterns
×

Disabling CSRF globally for all endpoints

Symptom
Traditional web forms become vulnerable to cross-site request forgery attacks. Users report unauthorized state changes — password resets, profile updates, order placements — that they did not initiate. The bug is silent until someone exploits it.
Fix
Disable CSRF only for stateless API paths where token-based authentication replaces cookie-based authentication: .csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")). Leave protection active for any path that a browser session touches.
×

Misordering request matchers in authorizeHttpRequests

Symptom
Public endpoints consistently return 403 Forbidden. The application logs show authenticated users being denied access to paths that should be open. Developers assume a role configuration bug and waste hours chasing a permissions problem that is actually a rule ordering problem.
Fix
Declare matchers from most specific to least specific. /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+

Symptom
Compilation failures after upgrading to Spring Boot 3.x. If the class is on the classpath via a transitive dependency it may compile but produce unexpected behavior because the auto-configuration no longer recognizes the old extension pattern.
Fix
Migrate to component-based configuration by declaring SecurityFilterChain beans annotated with @Bean. The lambda DSL available in HttpSecurity covers every use case the adapter supported, usually with less code.
×

Storing passwords with MD5 or SHA-256 without per-password salting

Symptom
A database breach exposes all user credentials in hours via precomputed rainbow tables. The failure is discovered externally — from breach notification services or security researchers — not from internal monitoring, because the application never logs password verification failures at that level.
Fix
Use BCryptPasswordEncoder with strength factor 10 to 12. Never implement your own hashing. Never use a single global salt — BCrypt generates a cryptographically random salt per password automatically.
×

Not handling authentication and authorization exceptions explicitly

Symptom
Security rejections return HTML error pages or stack traces to API clients. Clients receive 500 instead of 401 or 403. Internal exception details, framework versions, and sometimes partial stack traces leak in error responses — directly useful to an attacker mapping your infrastructure.
Fix
Implement a custom AuthenticationEntryPoint returning a structured JSON 401 for unauthenticated requests, and a custom AccessDeniedHandler returning a structured JSON 403 for insufficient permissions. Wire them into the filter chain via exceptionHandling() in your SecurityFilterChain configuration.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between Authentication (Who are you?) and Authori...
Q02SENIOR
Explain the role of the SecurityContextHolder and how it uses ThreadLoca...
Q03SENIOR
Can you explain how the DelegatingFilterProxy works within the Servlet C...
Q04SENIOR
Why is BCrypt preferred over MD5 or SHA-256 for password storage? Discus...
Q05SENIOR
How does CSRF protection work in Spring Security, and under what specifi...
Q01 of 05JUNIOR

What is the difference between Authentication (Who are you?) and Authorization (What can you do?) in the context of Spring Security?

ANSWER
Authentication verifies identity — it answers whether you are who you claim to be. In Spring Security, a successful authentication produces an 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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Does Spring Security slow down my application?
02
Can I use Spring Security without Spring Boot?
03
What is the difference between a Role and an Authority?
04
How do I test Spring Security configurations?
05
Why is my custom UserDetailsService being ignored?
🔥

That's Spring Boot. Mark it forged?

13 min read · try the examples if you haven't

Previous
Spring Boot Validation with Bean Validation API
9 / 15 · Spring Boot
Next
JWT Authentication with Spring Boot