This article demystifies Spring Security by examining its internal flow from request interception to controller execution. It explains the filter chain mechanism, authentication process, authorization decisions, and key components like AuthenticationManager, UserDetailsService, and PasswordEncoder.
If you have worked with Spring Boot for a while, you have used Spring Security without fully tracing what happens inside it. You add a dependency, configure a SecurityFilterChain, and wire a UserDetailsService, and your APIs are suddenly protected. It works. But under the hood, there is a very disciplined flow that decides who the user is, whether the password is valid, and whether the request should even reach your controller. Once that internal flow clicks, Spring Security stops feeling magical and starts feeling predictable.
The Big Picture Every incoming HTTP request does not go straight to your controller. Before that request reaches DispatcherServlet, it passes through the servlet filter chain. Spring Security plugs itself into that chain and intercepts the request early. That matters because security decisions should happen before business logic runs. The flow looks like this:
That is the full security journey in one line: intercept, authenticate, authorize, then continue.
Step 1: The Request Enters the Servlet Filter Chain When a client sends a request, Tomcat receives it first. Tomcat then passes it through a chain of servlet filters. These filters are not specific to Spring Security. They are part of the servlet infrastructure. Any framework can register filters here. Spring Security registers one important filter called FilterChainProxy. You can think of FilterChainProxy as the front desk for all Spring Security logic. It does not do all the security work itself. Instead, it decides which internal security filters should handle the request.
Step 2: FilterChainProxy Picks the Right SecurityFilterChain This is a key part that many developers miss. Spring Security does not always use one universal chain for every request. It can maintain multiple SecurityFilterChain configurations, each tied to different URL patterns or request matchers. For example:
/api/** may use JWT authentication /admin/** may require stricter role checks /login may use a form login
FilterChainProxy checks the request and selects the correct chain using RequestMatcher. That means Spring Security is not just a collection of filters. It is a smart router for security filters.
Step 3: Authentication Filter Extracts Credentials Once the correct security chain is selected, one of the authentication filters takes over. In the classic username-password login flow, that filter is usually UsernamePasswordAuthenticationFilter. Its job is simple:
Read the username and password from the request Create an unauthenticated Authentication object Pass that object to AuthenticationManager
At this point, the user is not yet trusted. The filter has only collected credentials. Verification still has to happen. This distinction is important. Extracting credentials and validating credentials are two separate responsibilities.
Step 4: AuthenticationManager Coordinates Authentication AuthenticationManager is the entry point for authentication logic. In most applications, the default implementation is ProviderManager. ProviderManager does not usually authenticate the user directly. Instead, it delegates to one of the configured AuthenticationProvider implementations. That design makes Spring Security flexible. Different providers can handle different authentication mechanisms:
username and password JWT token OAuth2 login LDAP custom authentication rules
When ProviderManager receives an authentication object, it loops through the registered providers and calls supports() on each one. The first provider that says, "Yes, I know how to handle this type of authentication," gets the job. Then authenticate() is called on that provider.
Step 5: AuthenticationProvider Verifies the User This is where the real authentication work happens. For username-password login, the provider is often DaoAuthenticationProvider. Its job usually includes two things:
Load the user from a data source Validate the submitted password
To load the user, it calls UserDetailsService. To validate the password, it uses PasswordEncoder. This split is one of the reasons Spring Security is so clean internally. Fetching user data and checking password hashing are handled by dedicated components, not mixed into one giant class.
Step 6: UserDetailsService Loads the User From the Database UserDetailsService is a very small but important contract. Its core method is:
loadUserByUsername(String username)
This method is responsible for fetching the user from your database, external system, or custom source. It returns a UserDetails object that contains:
username password roles or authorities account status flags, such as locked or disabled
If the user is not found, Spring Security throws an exception, and authentication fails. At this stage, the system now knows what the stored user record looks like. Next, it needs to compare the password.
Step 7: PasswordEncoder Validates the Password Spring Security does not compare raw passwords directly. Instead, it uses a PasswordEncoder such as BCryptPasswordEncoder. Here is the idea:
The password submitted by the client is plain text The stored password in the database is hashed PasswordEncoder.matches(rawPassword, encodedPassword) checks if they match safely
If the password is wrong, authentication fails. If it matches, Spring Security creates a fully authenticated Authentication object containing the user's identity and authorities. That object is now trusted.
Step 8: SecurityContextHolder Stores the Authenticated User Once authentication succeeds, Spring Security stores the authenticated user in SecurityContextHolder. This is what makes the user available for the rest of the request lifecycle. From here, other parts of the application can access the logged-in user through:
SecurityContextHolder.getContext().getAuthentication() @AuthenticationPrincipal Principal in controllers
In a regular servlet application, this security context is usually stored per request thread. That is why the controller can later know who the current user is without manually passing user details around.
Step 9: Authorization Happens Before the Controller Authentication answers this question: Who are you? Authorization answers this one: What are you allowed to do?
After the user is authenticated, Spring Security moves to authorization filters such as AuthorizationFilter. This stage checks whether the current user has the required role, authority, or permission for the requested resource. Examples:
hasRole("ADMIN") hasAuthority("PAYMENT_READ") Request matchers that restrict endpoints
If authorization fails, Spring Security stops the request and returns an error such as 403 Forbidden. If authorization succeeds, the request continues.
Step 10: The Request Reaches DispatcherServlet and Then the Controller Only after authentication and authorization are complete does the request proceed to Spring MVC. Now, DispatcherServlet can route the request to the correct controller. At this point, your controller can safely assume one of two things:
The endpoint is public, or The user has already been authenticated and authorized
That separation is why controllers stay cleaner. Security is handled earlier in the pipeline instead of being scattered across business logic.
What Happens If Authentication Fails? If authentication fails anywhere in the chain, Spring Security throws an authentication-related exception. Common outcomes include:
401 Unauthorized for unauthenticated access Redirect to the login page in form-based login custom error response in REST APIs
The controller is never called. This is a useful mental model: failed authentication stops the request before business logic begins.
What Makes Spring Security Feel Complex? Usually, it is not the concepts. It is the number of moving parts. There are many classes involved:
FilterChainProxy SecurityFilterChain UsernamePasswordAuthenticationFilter AuthenticationManager ProviderManager AuthenticationProvider UserDetailsService PasswordEncoder SecurityContextHolder AuthorizationFilter
At first glance, that looks like a lot. But if you group them by responsibility, it becomes manageable:
_Filters _handle request interception _Manager _and providers handle authentication delegation _UserDetailsService _and PasswordEncoder validate identity _SecurityContextHolder _stores the authenticated user _Authorization _filters enforce access rules
That is really the whole story.
A Simple Way to Remember the Flow Use this line:
Request comes in -> filter intercepts -> credentials extracted -> manager delegates -> provider authenticates -> context stores user -> authorization checks access -> controller runs
If you remember that sentence, you already understand the internals better than many developers who use Spring Security every day.
Final Takeaway Spring Security works like a layered checkpoint system. It intercepts the request before your application code sees it, verifies identity using providers and encoders, stores the authenticated user in a security context, checks permissions, and only then allows the request to hit the controller. Once you understand that flow, the framework feels a lot less intimidating.
Sonar PROMOTED Vibe check: Do developers trust AI? Based on a survey of over 1,100 developers, Sonar's newest State of Code report analyzes the impact of generative AI on software engineering workflows and how developers are adapting to address it. Read survey findings →

Comments
Please log in or register to join the discussion