Every request to a Spring application passes through a chain of servlet filters before it ever reaches your controller. Spring Security is nothing more than a well-ordered subset of that chain. Once you can name the filters and see the order they run in, authentication and authorization stop feeling like magic.

From the socket to the filter chain

The servlet container hands the request to a single DelegatingFilterProxy, which bridges the container world and the Spring context. It delegates to the FilterChainProxy, and that proxy picks the first SecurityFilterChain whose matcher accepts the request. An application can define several chains, one per group of routes, and only the first match runs.

Inside a SecurityFilterChain

A chain is an ordered list of single-responsibility filters. SecurityContextHolderFilter loads any existing authentication, CsrfFilter guards state-changing requests, an authentication filter tries to establish who you are, ExceptionTranslationFilter turns security exceptions into 401 or 403 responses, and AuthorizationFilter decides whether you are allowed through. Order is the whole design.

@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    return http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth -> oauth.jwt(withDefaults()))
        .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
        .build();
}

Authentication, then authorization

The authentication filter builds an unauthenticated Authentication token from the request and passes it to the AuthenticationManager. The default ProviderManager walks a list of AuthenticationProviders until one can handle the token. On success the result is stored in the SecurityContext; on failure the ExceptionTranslationFilter takes over. Authorization is a separate, later step: AuthorizationFilter asks an AuthorizationManager whether the now-known principal may proceed.

@PreAuthorize("hasAuthority('SCOPE_documents:sign')")
public SignedDocument sign(DocumentRequest req) { ... }

And finally, the controller

Only after AuthorizationFilter passes does the request continue to the DispatcherServlet, the handler mapping and your controller. By the time your method runs the SecurityContext is already populated, so you never re-check the token yourself. You just read the authenticated principal.

@GetMapping("/api/me")
Profile me(@AuthenticationPrincipal Jwt jwt) {
    return profiles.forSubject(jwt.getSubject());
}

The authentication modalities

The pipeline above is fixed, but the authentication filter you plug into it is a choice. Spring Security ships first-class support for several modalities, and you can combine them per chain.

Form login and HTTP Basic

The classic pair. Form login renders or accepts a login form and keeps a server-side session, which suits server-rendered apps. HTTP Basic sends credentials on every request and is handy for internal tools and quick tests, always behind TLS.

OAuth2 and OpenID Connect login

Here your app is the client. The user authenticates with an external provider, and Spring handles the redirect dance, the token exchange and the OIDC user info. This is the modern default for delegated sign-in with Google, GitHub or a corporate identity provider.

Resource server: JWT and opaque tokens

For APIs, the app is a resource server. It does not log anyone in; it validates a bearer token on each request and maps its claims to authorities. A JWT is verified locally against the provider's keys, while an opaque token is introspected at the provider. Both lead to the stateless configuration shown above.

Passkeys, one-time tokens and SAML2

Newer chains can start with WebAuthn passkeys for phishing-resistant sign-in, one-time tokens for passwordless email links, or SAML2 for enterprise single sign-on. They are different first filters feeding the very same authentication and authorization steps.

Security is not a filter you bolt on at the end. It is the order the filters run in.

Pick the modality that matches the client, let the chain do the rest, and keep your controllers free of security plumbing. That separation is the whole point of the design.