Performance: ~1-5ms overhead per request from filter chain processing
Biggest mistake: Disabling CSRF globally without understanding the stateless API exception
✦ Definition~90s read
What is Spring Boot Security Basics?
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.
★
Think of Spring Boot Security Basics as a powerful tool in your developer toolkit.
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.
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;
importstatic org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecuritypublicclassSecurityConfig {
/**
* io.thecodeforge: StandardFilterChainConfiguration
*
* Rule ordering is intentional and matters:
* 1. Most specific path matchers come first.
* 2. Broadermatchers (anyRequest) always come last.
*
* Swapping that order causes SpringSecurity to apply the
* broader rule first, silently locking out public endpoints.
*
* CSRF is disabled here because this config targets a
* stateless RESTAPI using token-based auth. DoNOT
* disable CSRFfor session-based web applications.
*/
@BeanpublicSecurityFilterChainforgeSecurityFilterChain(HttpSecurity http) throwsException {
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.
thecodeforge.io
Spring Boot Security Basics
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.
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.
Spring Security default filter chain order (simplified)
Authentication vs Authorization: Understanding the Difference
Authentication and authorization are the two pillars of security, but they are frequently confused — even in production code. Authentication answers who you are (identity verification), while authorization answers what you can do (access permission).
The following comparison table clarifies the distinction and shows how each concept maps to Spring Security components:
Aspect
Authentication
Authorization
Purpose
Verify the identity of a user or system
Determine what the authenticated identity is allowed to access
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;
@RestControllerpublicclassAuthController {
@GetMapping("/whoami")
publicStringwhoAmI() {
// Authentication object from SecurityContextAuthentication 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.
thecodeforge.io
Spring Boot Security Basics
Authentication Methods Comparison: Form Login, HTTP Basic, JWT, OAuth2
Choosing the right authentication method for your Spring Boot application depends on your architecture, client type, and security requirements. Each method has different trade-offs in terms of session management, statefulness, and use-case fit.
The table below compares the four most common approaches:
Method
Session/Stateless
When to Use
Spring Security Configuration
Pros
Cons
Form Login
Session-based (stateful)
Traditional server-rendered web apps where users submit credentials via a form
.formLogin() with optional custom login page
- Simple to set up
HTTP Basic
Stateless (credentials sent with every request)
Simple internal APIs, monitoring endpoints, or prototyping
.httpBasic()
- Very simple to implement
JWT (Bearer Token)
Stateless (token in Authorization header)
REST APIs, single-page applications, mobile apps
Custom filter before UsernamePasswordAuthenticationFilter
- Stateless, scalable
OAuth2 / OpenID Connect
Delegated (stateless or session-based depending on grant type)
Applications that need to authenticate via external providers (Google, GitHub, etc.) or enable single sign-on
.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.
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:
Advantage
Explanation
Battle-tested defaults
Secure defaults for CSRF, session fixation, clickjacking, and headers — you get protection against OWASP Top 10 without configuration.
Centralized configuration
All security rules in one place (SecurityFilterChain beans). No scattered role checks in business logic.
Comprehensive authentication providers
Supports form login, HTTP Basic, Digest, LDAP, OAuth2, SAML, JWT, and custom providers via AuthenticationProvider interface.
Fine-grained authorization
URL-based (authorizeHttpRequests) and method-level (@PreAuthorize, @Secured, @PostAuthorize) access control.
Testability
@WithMockUser, @WithAnonymousUser, and MockMvc integration make security testing straightforward.
Production incident patterns
Widely deployed — many edge cases (BCrypt truncation, thread-local propagation, session replication) are documented.
Disadvantages:
Disadvantage
Explanation
Steep learning curve
The filter chain, multiple configuration styles, and deprecation cycles (e.g., WebSecurityConfigurerAdapter removal) confuse newcomers.
Configuration complexity
Real-world setups often require multiple SecurityFilterChain beans, custom filters, and careful ordering — easy to misconfigure.
Performance overhead
Filter chain adds 1-5ms per request; BCrypt at strength 12 adds ~250ms on login. Not negligible for high-throughput systems.
Over-engineering trap
It's tempting to add OAuth2 or complex JWT infrastructure when simple HTTP Basic would suffice for internal tools.
XML/annotation legacy
Older configuration styles (XML, pre-lambda DSL) are still in many codebases, creating migration burden.
Async context handling
SecurityContextHolder defaults to ThreadLocal, which breaks in async scenarios unless you explicitly propagate context.
The key takeaway: Spring Security is excellent at what it does — providing a comprehensive, extensible security framework — but it expects you to understand its architecture. The disadvantages primarily stem from misuse or underestimating the learning curve.
For most Spring Boot applications, the advantages far outweigh the disadvantages, especially once you invest the time to understand the filter chain model. The alternative — rolling your own security — almost always results in worse security and higher maintenance cost.
🔥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;
@ServicepublicclassTransferService {
/**
* Only users with AUTHORIZED_SIGNER permission can execute transfers.
* The method will throwAccessDeniedExceptionif the caller lacks this authority.
*/
@PreAuthorize("hasPermission(#accountId, 'TRANSFER')")
publicvoidtransferFunds(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;
@ConfigurationpublicclassPasswordConfig {
/**
* io.thecodeforge: Password encoding strategy.
*
* WHYBCrypt:
* - 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
*
* WHYNOTMD5 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
*
* STRENGTHFACTORGUIDANCE:
* - 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.
*/
@BeanpublicPasswordEncoderforgePasswordEncoder() {
returnnewBCryptPasswordEncoder(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.
The SecurityContext — Your App's Truth About Who's Asking
Every authenticated request deposits a SecurityContext into a ThreadLocal. That object holds the Authentication — the principal (who), the credentials (proof), and the granted authorities (what they can do). If you blindly trust that context without verifying it came through the filter chain, you're one FilterOrderRegistration bug away from privilege escalation. The SecurityContext is set by the SecurityContextPersistenceFilter before your controllers run, and it's cleared after the response commits. Never stash it in a static field or a session scoped bean. Always extract via SecurityContextHolder.getContext().getAuthentication(). If you call that from a background thread, it's null — pass the context explicitly or switch to DelegatingSecurityContextRunnable. This is why async methods break your @PreAuthorize annotations: the child thread has no auth context.
SecurityContextExtractor.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
// io.thecodeforge
@ServicepublicclassSecurityContextExtractor {
// NEVER do thispublicStringgetUsernameFromStatic() {
returnSecurityContextHolder.getContext()
.getAuthentication().getName();
}
// DO this — works only on the request threadpublicStringgetCurrentUser() {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
thrownewIllegalStateException("No authenticated principal");
}
return auth.getName();
}
// For async: pass the context explicitly
@AsyncpublicvoidprocessAsync(Authentication auth) {
SecurityContextHolder.getContext().setAuthentication(auth);
try {
// business logic
} finally {
SecurityContextHolder.clearContext();
}
}
}
Sysouts or logs that call SecurityContextHolder.getContext() in a background thread will silently return null. Use SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL) only if you understand the memory leak risk for long-lived threads.
🎯 Key Takeaway
SecurityContext lives on the request thread. Async kills it. Pass it or wrap it.
thecodeforge.io
Spring Boot Security Basics
CSRF Protection: Why Your REST API Probably Doesn't Need It
CSRF (Cross-Site Request Forgery) exploits the fact that browsers automatically attach cookies to every request to a domain. If your app uses cookie-based authentication — form login or JSESSIONID — an attacker can trick a logged-in user into submitting a malicious request. Spring Security enables CSRF protection by default for any state-changing endpoint (POST, PUT, DELETE). For a server-rendered Thymeleaf app, that's correct. For a REST API that uses Bearer tokens (JWT, OAuth2), it's noise. The token isn't sent automatically by the browser — it's in the Authorization header, which JavaScript must explicitly attach. So if you're building a SPA with Angular/React that sends JWT, disable CSRF. Otherwise every POST to your API throws a 403 Forbidden. That wasted four hours debugging is a rite of passage. Save yourself.
GET /api/orders 200 OK | POST /api/orders with token 201 Created | POST /api/orders without token 401 Unauthorized
🔥Production Trap:
If you disable CSRF but keep JSESSIONID-based auth, you've opened a CSRF vector. Only disable CSRF when authentication uses a header-based token (Bearer, API key) that browsers don't auto-send.
🎯 Key Takeaway
CSRF protects cookie-based auth. JWT APIs don't need it. Disabling it without understanding why is a security antipattern.
● 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
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.
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
Aspect
Manual Security Check
Spring Boot Security
Maintenance
Hard — security logic is spread across dozens of controllers and services; adding a new role requires touching multiple files
Easy — centralized in a single SecurityFilterChain bean; adding a new rule is a one-line change in one file
Standardization
Ad-hoc — high risk of bypasses, missed endpoints, and edge-case errors that only surface under specific conditions
Standard — uses industry-proven patterns and ships secure defaults aligned with OWASP recommendations
Vulnerability Protection
Manual — every developer must independently implement CSRF validation, XSS headers, and clickjacking protection
Built-in — defaults protect against the most common OWASP Top 10 vectors without any additional configuration
Flexibility
Low — swapping from Basic Auth to JWT requires refactoring security logic out of every controller that embedded it
High — authentication provider changes are localized to the SecurityFilterChain; controllers are unaffected
Auditability
Difficult — tracing who can access what requires reading through every controller, service, and utility class
Clear — declarative security rules in one location make access policy readable by anyone who can open SecurityConfig.java
Predictable — roughly 1 to 5ms overhead per request from filter chain processing, independent of application complexity
Testing Complexity
High — each controller's security logic must be tested independently; gaps are common because coverage is manual
Low — configure once, test the configuration, and security applies globally; @WithMockUser handles most scenarios
⚙ Quick Reference
7 commands from this guide
File
Command / Code
Purpose
SecurityConfig.java
@Configuration
What Is Spring Boot Security Basics and Why Does It Exist?
CustomFilterPlacementConfig.java
@Configuration
Spring Security Filter Chain
AuthenticationAuthorizationExample.java
@RestController
Authentication vs Authorization
MultipleAuthConfig.java
@Configuration
Authentication Methods Comparison
PreAuthorizeMethodSecurityExample.java
@Service
5 Practice Projects to Master Spring Security
PasswordEncoderConfig.java
@Configuration
Common Mistakes and How to Avoid Them
SecurityContextExtractor.java
@Service
The SecurityContext
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.
Q02 of 05SENIOR
Explain the role of the SecurityContextHolder and how it uses ThreadLocal to store user details during a request.
ANSWER
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.
Q03 of 05SENIOR
Can you explain how the DelegatingFilterProxy works within the Servlet Container to hand off requests to Spring-managed beans?
ANSWER
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.
Q04 of 05SENIOR
Why is BCrypt preferred over MD5 or SHA-256 for password storage? Discuss cost factors and salting mechanism.
ANSWER
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.
Q05 of 05SENIOR
How does CSRF protection work in Spring Security, and under what specific conditions is it safe to disable it in a REST API?
ANSWER
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.
01
What is the difference between Authentication (Who are you?) and Authorization (What can you do?) in the context of Spring Security?
JUNIOR
02
Explain the role of the SecurityContextHolder and how it uses ThreadLocal to store user details during a request.
SENIOR
03
Can you explain how the DelegatingFilterProxy works within the Servlet Container to hand off requests to Spring-managed beans?
SENIOR
04
Why is BCrypt preferred over MD5 or SHA-256 for password storage? Discuss cost factors and salting mechanism.
SENIOR
05
How does CSRF protection work in Spring Security, and under what specific conditions is it safe to disable it in a REST API?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
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.
Was this helpful?
02
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.
Was this helpful?
03
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.
Was this helpful?
04
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.
Was this helpful?
05
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.