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:

  1. Creates a SecurityFilterChain that secures all endpoints
  2. Enables HTTP Basic authentication
  3. Generates a random password and prints it at startup
  4. 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
ConceptWhat it is
DelegatingFilterProxyBridge between Servlet container and Spring context
FilterChainProxySpring bean that holds all SecurityFilterChain instances
SecurityFilterChainA list of security filters for a matching URL pattern
SecurityContextHolds the current Authentication for the request thread
AuthenticationThe result of authentication — principal + authorities + credentials
AuthenticationManagerCoordinates authentication attempts across providers
AuthenticationProviderDoes the actual credential verification
UserDetailsServiceLoads user data (username, password, roles) from your data store
AuthorizationFilterFinal filter — decides allow or deny based on authorities

Summary

  • Spring Security is a chain of Servlet filters, not magic.
  • DelegatingFilterProxyFilterChainProxySecurityFilterChain → individual filters.
  • Each request flows through the filter chain: authentication filters run first, then the authorization filter at the end.
  • The SecurityContext (stored in ThreadLocal) holds the Authentication object for the current request.
  • AuthenticationManagerAuthenticationProviderUserDetailsService is the authentication delegation chain.
  • The modern configuration API uses a SecurityFilterChain @Bean with lambda DSL — no WebSecurityConfigurerAdapter.

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.