How Spring Security Works: The Big Picture
Why Spring Security Is Hard to Learn
Spring Security is not complex because the concepts are complex. Authentication and authorization are straightforward ideas. Spring Security is hard because it hides its machinery: a request comes in, something happens, an endpoint is secured or not — and without knowing the internal architecture, the behaviour feels magical and the configuration feels arbitrary.
This article removes the magic. By the end, you will have the mental model needed to understand every other concept in this series.
What Spring Security Does
Spring Security adds two capabilities to your application:
Authentication — Verifying who the user is.
“Is this really Alice? Verify her credentials.”
Authorization — Verifying what the user is allowed to do.
“Alice is authenticated. Is she allowed to access
/admin/reports?”
Everything else Spring Security does (JWT, OAuth2, CSRF, session management, headers) is infrastructure that supports or extends these two core jobs.
How It Plugs into the Servlet Stack
A Spring Boot web application is built on the Servlet API. Every HTTP request goes through a chain of Filters before reaching a Servlet (the DispatcherServlet, which routes to your controllers).
sequenceDiagram
participant C as Client (Browser/App)
participant FC as Servlet Filter Chain
participant DS as DispatcherServlet
participant Ctrl as @RestController
C->>FC: HTTP Request
FC->>FC: Filter 1 (logging, etc.)
FC->>FC: Filter 2 (Spring Security)
FC->>DS: Allowed Request
DS->>Ctrl: Route to handler
Ctrl-->>DS: Response
DS-->>FC: Response
FC-->>C: HTTP Response
Spring Security inserts itself as one of those filters. But because security requires many concerns (authentication, authorization, CSRF, headers, session…), it is actually a chain of filters within a filter — a dedicated sub-chain managed by a special delegating filter.
The DelegatingFilterProxy
Spring Security registers a single standard Servlet Filter called DelegatingFilterProxy. Its only job is to bridge the Servlet container’s filter chain with Spring’s ApplicationContext:
flowchart LR
A[Servlet Container] -->|registered filter| B[DelegatingFilterProxy]
B -->|looks up bean by name| C[FilterChainProxy\nin Spring Context]
C --> D[SecurityFilterChain 1]
C --> E[SecurityFilterChain 2]
DelegatingFilterProxy is a thin bridge. The real work is done by FilterChainProxy — a Spring bean that holds one or more SecurityFilterChain beans.
FilterChainProxy and SecurityFilterChain
FilterChainProxy holds a list of SecurityFilterChain objects. When a request arrives, it asks each SecurityFilterChain in order: “Does this chain match this request?” The first matching chain handles the request.
flowchart TD
Request[HTTP Request] --> FCP[FilterChainProxy]
FCP -->|matches /api/**| SFC1[SecurityFilterChain 1\nfor REST API\njwt, stateless]
FCP -->|matches /**| SFC2[SecurityFilterChain 2\nfor web app\nform login, session]
SFC1 --> Filters1[Filter A → Filter B → Filter C]
SFC2 --> Filters2[Filter D → Filter E → Filter F]
This is powerful: different parts of your application can have different security configurations. Your REST API can be stateless + JWT, while your admin web UI uses form login and sessions — in the same Spring Boot app.
Each SecurityFilterChain is a list of Spring Security filters. A typical chain has ~15 built-in filters, each handling one concern.
The Core Security Filters (in Order)
Here are the most important filters in a typical SecurityFilterChain, in the order they execute:
flowchart TD
R[HTTP Request] --> F1[DisableEncodeUrlFilter\nPrevents session ID in URLs]
F1 --> F2[SecurityContextHolderFilter\nLoads SecurityContext from storage]
F2 --> F3[UsernamePasswordAuthenticationFilter\nHandles POST /login]
F3 --> F4[BasicAuthenticationFilter\nHandles Authorization: Basic header]
F4 --> F5[BearerTokenAuthenticationFilter\nHandles Authorization: Bearer JWT]
F5 --> F6[RememberMeAuthenticationFilter\nHandles remember-me cookies]
F6 --> F7[AnonymousAuthenticationFilter\nSets anonymous if not authenticated]
F7 --> F8[SessionManagementFilter\nSession fixation, concurrency]
F8 --> F9[ExceptionTranslationFilter\nConverts AccessDeniedException → 403\nAuthenticationException → redirect/401]
F9 --> F10[AuthorizationFilter\nChecks if authenticated user can access this URL]
F10 --> DS[DispatcherServlet → Controller]
Only relevant filters are active in each chain. If you don’t configure form login, UsernamePasswordAuthenticationFilter is not added. If you configure JWT, BearerTokenAuthenticationFilter is added. You control the filter list through the SecurityFilterChain configuration.
The SecurityContext: Where Authentication Lives
Once a user is authenticated, Spring Security stores the result in the SecurityContext. The SecurityContextHolder gives access to the current context from anywhere in your code:
classDiagram
class SecurityContextHolder {
+getContext() SecurityContext
+setContext(SecurityContext)
+clearContext()
}
class SecurityContext {
+getAuthentication() Authentication
+setAuthentication(Authentication)
}
class Authentication {
+getPrincipal() Object
+getCredentials() Object
+getAuthorities() Collection
+isAuthenticated() boolean
}
class UserDetails {
+getUsername() String
+getPassword() String
+getAuthorities() Collection
}
SecurityContextHolder --> SecurityContext
SecurityContext --> Authentication
Authentication --> UserDetails : principal
By default, SecurityContextHolder uses a ThreadLocal — each HTTP request thread has its own SecurityContext. When the request ends, the context is cleared.
// Access the current user anywhere in your application
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // "alice"
Collection<?> roles = auth.getAuthorities(); // [ROLE_USER, ROLE_ADMIN]
UserDetails user = (UserDetails) auth.getPrincipal(); // full UserDetails object
Authentication vs Authorization Flow
Here is the complete flow from unauthenticated request to secured response:
sequenceDiagram
participant Client
participant SSF as Spring Security Filters
participant AM as AuthenticationManager
participant AP as AuthenticationProvider
participant UDS as UserDetailsService
participant AF as AuthorizationFilter
participant Ctrl as Controller
Client->>SSF: POST /login {username, password}
SSF->>AM: authenticate(token)
AM->>AP: authenticate(token)
AP->>UDS: loadUserByUsername("alice")
UDS-->>AP: UserDetails (alice, hashed_pw, [ROLE_USER])
AP->>AP: verify password matches
AP-->>AM: Authentication (authenticated=true)
AM-->>SSF: Authentication
SSF->>SSF: Store in SecurityContext
SSF-->>Client: 200 OK (session cookie or JWT)
Client->>SSF: GET /orders (with session/JWT)
SSF->>SSF: Load Authentication from SecurityContext
SSF->>AF: is ROLE_USER allowed on GET /orders?
AF-->>SSF: Yes
SSF->>Ctrl: Request proceeds
Ctrl-->>Client: 200 OK orders list
The key components:
- AuthenticationManager — coordinates authentication (delegates to providers)
- AuthenticationProvider — does the actual credential verification
- UserDetailsService — loads user data from your database
- AuthorizationFilter — checks whether the authenticated user can access the requested resource
What Happens With No Configuration
When you add spring-boot-starter-security with no configuration, Spring Boot’s auto-configuration activates. It:
- Creates a
SecurityFilterChainthat secures all endpoints - Enables HTTP Basic authentication
- Generates a random password and prints it at startup
- Creates a single user with username
user
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
This is intentional — fail secure. You must explicitly open endpoints, not close them.
Adding Spring Security to Your Project
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
For testing:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Your First SecurityFilterChain
The minimum modern configuration — a @Bean returning SecurityFilterChain:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login", "/register").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.logout(Customizer.withDefaults());
return http.build();
}
}
This is the modern API — no WebSecurityConfigurerAdapter, no .and() chaining. The lambda DSL makes each concern self-contained.
What this does:
/public/**,/login,/register→ open to everyone- Everything else → must be authenticated
- Enables form login with Spring’s default login page
- Enables logout at
GET /logout
Key Concepts Summary
mindmap
root((Spring Security))
Architecture
DelegatingFilterProxy
FilterChainProxy
SecurityFilterChain
Filter Order
Authentication
AuthenticationManager
AuthenticationProvider
UserDetailsService
SecurityContext
Authorization
AuthorizationFilter
GrantedAuthority
Role vs Authority
Configuration
SecurityFilterChain bean
HttpSecurity lambda DSL
No WebSecurityConfigurerAdapter
| Concept | What it is |
|---|---|
DelegatingFilterProxy | Bridge between Servlet container and Spring context |
FilterChainProxy | Spring bean that holds all SecurityFilterChain instances |
SecurityFilterChain | A list of security filters for a matching URL pattern |
SecurityContext | Holds the current Authentication for the request thread |
Authentication | The result of authentication — principal + authorities + credentials |
AuthenticationManager | Coordinates authentication attempts across providers |
AuthenticationProvider | Does the actual credential verification |
UserDetailsService | Loads user data (username, password, roles) from your data store |
AuthorizationFilter | Final filter — decides allow or deny based on authorities |
Summary
- Spring Security is a chain of Servlet filters, not magic.
DelegatingFilterProxy→FilterChainProxy→SecurityFilterChain→ individual filters.- Each request flows through the filter chain: authentication filters run first, then the authorization filter at the end.
- The
SecurityContext(stored inThreadLocal) holds theAuthenticationobject for the current request. AuthenticationManager→AuthenticationProvider→UserDetailsServiceis the authentication delegation chain.- The modern configuration API uses a
SecurityFilterChain@Beanwith lambda DSL — noWebSecurityConfigurerAdapter.
Next: Article 2 goes deep on the filter chain — every built-in filter explained with the exact order, what each one does, and how to add your own.