Skip to content
Home Java Spring Boot Security Basics: Architecture and Implementation

Spring Boot Security Basics: Architecture and Implementation

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Spring Boot → Topic 9 of 15
A comprehensive guide to Spring Boot Security Basics — master authentication, authorization, and the Filter Chain architecture in modern Java applications.
⚙️ Intermediate — basic Java knowledge assumed
In this tutorial, you'll learn
A comprehensive guide to Spring Boot Security Basics — master authentication, authorization, and the Filter Chain architecture in modern Java applications.
  • 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.
  • 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.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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
🚨 START HERE
Spring Security Emergency Debugging
When security breaks in production, start here. Work top to bottom — do not skip steps.
🟡All requests return 403
Immediate ActionCheck 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 NowAdd `.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 ActionCheck 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 NowEnsure 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.
Production IncidentSession Fixation Attack in ProductionAfter a Spring Boot upgrade, users reported being logged into other users' accounts during peak traffic.
SymptomUsers intermittently see other users' data; session IDs do not change after authentication completes.
AssumptionSpring Security's default session fixation protection is enabled and working correctly across all nodes.
Root causeA 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.
FixRevert 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 guaranteedStaging environments that do not replicate production topology will miss an entire class of distributed session bugsTest session ID rotation explicitly after login — automate it in your CI pipeline, not just in manual QAMonitor 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 approach
403 Forbidden on all endpointsCheck 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.
Authentication object is null in controllerVerify 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.
BCrypt encoded password does not matchConfirm 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.
Custom UserDetailsService not calledConfirm 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.
JWT filter runs but SecurityContext is still empty after validationEnsure 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 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.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
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();
    }
}
Mental Model
The Filter Chain Mental Model
Think of Spring Security as a series of security checkpoints that every HTTP request must clear before your business logic ever runs. Each checkpoint is narrow in scope and independently configurable.
  • 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.

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.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637
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.
🗂 Security Implementation Comparison
Manual checks vs. Spring Security filter chain — what changes across the full development lifecycle
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

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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

    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 Questions on This Topic

  • QWhat is the difference between Authentication (Who are you?) and Authorization (What can you do?) in the context of Spring Security?JuniorReveal
    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.
  • QExplain the role of the SecurityContextHolder and how it uses ThreadLocal to store user details during a request.Mid-levelReveal
    SecurityContextHolder is the static access point for the security context — the container holding the current Authentication object. By default it uses MODE_THREADLOCAL, which means each thread gets its own isolated security context via a ThreadLocal<SecurityContext>. For synchronous servlet request handling, where one request maps to one thread for its full lifecycle, this works correctly. The problem surfaces in async contexts: if you hand work off to a different thread (via @Async, CompletableFuture, or a task executor), that thread has an empty security context because ThreadLocal does not propagate automatically. Solutions include switching to MODE_INHERITABLETHREADLOCAL for child threads, or wrapping async tasks with DelegatingSecurityContextRunnable / DelegatingSecurityContextExecutor to propagate context explicitly. Reactive applications using WebFlux cannot use thread-local storage at all — they use ReactiveSecurityContextHolder backed by the reactor context instead.
  • QCan you explain how the DelegatingFilterProxy works within the Servlet Container to hand off requests to Spring-managed beans?SeniorReveal
    The Servlet container manages filters through its own lifecycle — it instantiates them at startup and knows nothing about Spring's ApplicationContext. DelegatingFilterProxy solves that impedance mismatch. It registers itself with the container as a standard javax.servlet.Filter, but when a request arrives it looks up a named bean from the Spring ApplicationContext (typically springSecurityFilterChain) and delegates the actual filter work to it. That named bean is FilterChainProxy, which iterates over your registered SecurityFilterChain beans and delegates to the first chain whose RequestMatcher matches the incoming request URL. The result is that your security filters are full Spring beans — they can use @Autowired, participate in AOP, be scoped, and be tested with the Spring test context — while still operating within the standard servlet filter mechanism the container expects.
  • QWhy is BCrypt preferred over MD5 or SHA-256 for password storage? Discuss cost factors and salting mechanism.Mid-levelReveal
    MD5 and SHA-256 are general-purpose cryptographic hash functions optimized for speed. Speed is catastrophically wrong for passwords: modern GPUs can compute hundreds of billions of MD5 hashes per second, making brute-force and dictionary attacks against a leaked database feasible in hours. BCrypt is deliberately slow. Its cost factor (work factor) controls the number of iterations — each increment of 1 doubles the computation time. At strength 12, a single hash takes roughly 250ms on modern server hardware. An attacker with the same hardware can attempt roughly 4 hashes per second, versus billions for MD5. BCrypt also generates a cryptographically random 128-bit salt per password automatically and embeds it in the output hash. This means identical passwords produce different hashes, eliminating rainbow table attacks entirely. The adaptive nature of the cost factor is also significant: as hardware improves, you raise the factor and re-hash credentials at next login, maintaining the same resistance level indefinitely without changing your code.
  • QHow does CSRF protection work in Spring Security, and under what specific conditions is it safe to disable it in a REST API?SeniorReveal
    CSRF attacks exploit the fact that browsers automatically attach session cookies to any request matching the cookie's domain, regardless of where the request originates. An attacker hosts a page that makes a cross-origin POST to your application, the browser attaches the victim's session cookie, and the request executes with the victim's identity. Spring Security's CSRF protection breaks this by requiring a synchronizer token — a value generated server-side per session and embedded in forms or response headers — that must be present in state-changing requests. The browser's same-origin policy prevents the attacker's page from reading this token from a different origin, so the forged request cannot include it and is rejected. For stateless REST APIs using token-based authentication (JWT in the Authorization header, OAuth2 bearer tokens), CSRF protection is genuinely unnecessary. Browsers cannot set arbitrary Authorization headers on cross-origin requests without a preflight — and the preflight response controls whether the cross-origin request is permitted. Since the authentication credential is not automatically attached by the browser (unlike cookies), the CSRF attack vector does not exist for those endpoints. The safe and precise way to disable it is csrf.ignoringRequestMatchers("/api/**") rather than a global disable, so you retain protection if any session-based flows exist alongside the API.

Frequently Asked Questions

Does Spring Security slow down my application?

In practice, the overhead is 1 to 5ms per request from filter chain processing — negligible for the vast majority of applications. The number you should actually worry about is the BCrypt hashing time on your login endpoint: 250ms at strength 12. That is not a bug, it is the security property you are paying for. Factor it into your SLA for the authentication endpoint specifically, and make sure your infrastructure timeout settings account for it. For everything else, the filter chain overhead is below the noise floor of your network latency.

Can I use Spring Security without Spring Boot?

Yes. Spring Security predates Spring Boot and works with plain Spring MVC, Spring WebFlux, or any servlet-based application. Without Spring Boot, you configure it manually: register DelegatingFilterProxy in your servlet container, define FilterChainProxy, and wire your SecurityFilterChain beans yourself. Spring Boot's auto-configuration does all of that for you based on what it finds on the classpath. If you have ever wondered why adding spring-boot-starter-security to a project immediately secures all endpoints, that is auto-configuration at work — it registers a default SecurityFilterChain bean with conservative defaults.

What is the difference between a Role and an Authority?

Technically, both are implementations of GrantedAuthority — strings attached to an Authentication object that represent what the user is allowed to do. The distinction is a naming convention: Roles are authorities prefixed with ROLE_. When you call hasRole('ADMIN'), Spring Security internally looks for an authority named ROLE_ADMIN. When you call hasAuthority('ADMIN'), it looks for an authority named exactly ADMIN with no prefix transformation. The practical implication: if your database stores role values as ADMIN without the prefix, use hasAuthority() or add the prefix in your UserDetailsService implementation. Mixing the two conventions in the same application is a reliable source of silent authorization failures.

How do I test Spring Security configurations?

The Spring Security Test module provides @WithMockUser and @WithAnonymousUser for populating the security context in unit and slice tests without going through actual authentication. For integration tests, use MockMvc with SecurityMockMvcConfigurers.springSecurity() applied to the mock MVC builder — this runs the full filter chain against your mock requests. Test both the positive case (access granted with correct credentials and role) and the negative cases (unauthenticated request returns 401, authenticated user with wrong role returns 403). Testing only the happy path catches roughly half the bugs a security configuration can contain.

Why is my custom UserDetailsService being ignored?

The most common cause is Spring Security's auto-configuration registering an InMemoryUserDetailsManager before it finds your custom bean. This happens when you declare @Service on your implementation but do not explicitly wire it into an AuthenticationProvider or AuthenticationManagerBuilder. In Spring Boot 3.x, declaring your UserDetailsService as a @Bean in a @Configuration class and ensuring no InMemoryUserDetailsManager bean exists alongside it is usually sufficient. If you have multiple AuthenticationProvider beans registered, check which one is selected — the ProviderManager tries providers in order and stops at the first one that supports the token type, which may not be the one backed by your service.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousSpring Boot Validation with Bean Validation APINext →JWT Authentication with Spring Boot
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged