Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,050visitors

Built for engineers who want to go deep.


← Spring Security & JWT Auth

Spring Security Basics

  • Spring Security Overview
  • JWT Authentication
  • Authentication Mechanics

Authorization

  • Role-Based Access Control
  • Password Encoding
  • OAuth2 & Social Login Basics
Chaturmind
← Spring Security & JWT Auth

Spring Security Basics

  • Spring Security Overview
  • JWT Authentication
  • Authentication Mechanics

Authorization

  • Role-Based Access Control
  • Password Encoding
  • OAuth2 & Social Login Basics
HomeLearnSpring BootSpring Security & JWT AuthSpring Security Basics
✓ FreeIntermediate· 10 min read

Spring Security Overview

Filter chains, SecurityContext, authentication vs authorization — how Spring Security is wired.

Published September 21, 2026


Spring Security Overview

Spring Security is the standard security framework for Spring Boot applications. It handles authentication (who are you?) and authorization (what are you allowed to do?) through a chain of servlet filters.

The Security Filter Chain

Every HTTP request passes through a chain of filters before reaching your controller:

HTTP Request
    ↓
SecurityContextPersistenceFilter  (load/save SecurityContext)
    ↓
UsernamePasswordAuthenticationFilter  (form login)
    ↓
BearerTokenAuthenticationFilter  (JWT/OAuth2)
    ↓
ExceptionTranslationFilter  (handle 401/403)
    ↓
AuthorizationFilter  (check permissions)
    ↓
Your Controller

DelegatingFilterProxy — bridging servlet filters to Spring beans

Servlet containers (Tomcat) manage filters through their own lifecycle, entirely separate from Spring's IoC container — DelegatingFilterProxy is the bridge: a thin servlet Filter registered with the container that does nothing itself except look up a real, Spring-managed bean (FilterChainProxy, which wraps the actual security filter chain) and delegate every call to it. This is what lets Spring Security's filters be ordinary Spring beans — dependency-injected, configured via @Bean methods — while still participating correctly in the servlet container's own filter lifecycle.

Multiple SecurityFilterChains, matched by request pattern

@Bean
@Order(1)
SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    return http.securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .build();
}

@Bean
@Order(2)
SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception {
    return http.securityMatcher("/public/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
        .build();
}

A single application can register several SecurityFilterChain beans, each scoped to a different securityMatcher pattern — the first chain whose pattern matches the incoming request handles it entirely. This lets /api/** require authentication while /public/** stays open, without either configuration interfering with the other.

Filter ordering and short-circuiting

Filters run in a strict, fixed order (SecurityContextPersistenceFilter → CsrfFilter → UsernamePasswordAuthenticationFilter → ... → AuthorizationFilter). Each filter can either call chain.doFilter() to pass the request to the next filter (pass through), or write a response directly and never call doFilter() (short-circuit) — this is Chain of Responsibility applied directly (see Chain of Responsibility), and it's exactly why filter order matters: a filter earlier in the chain can reject a request before any later filter (including the actual authorization check) ever runs.

ExceptionTranslationFilter

Sits specifically to catch AuthenticationException and AccessDeniedException thrown by later filters, translating them into the correct HTTP response — an unauthenticated request attempting a protected resource becomes a 401, an authenticated-but-unauthorized request becomes a 403. Without this filter, those exceptions would otherwise propagate as generic 500 errors.

FilterSecurityInterceptor / AuthorizationFilter — the final access decision

The last filter in the standard chain, making the actual "is this request allowed" decision based on the configured authorization rules — everything before it (authentication filters, CSRF, exception translation) exists to get a correctly-populated Authentication object to this final gate.

Servlet Filter vs Spring HandlerInterceptor

A Filter operates at the servlet container level, before the request even reaches Spring MVC's DispatcherServlet — it has no knowledge of which controller method will eventually handle the request. A Spring HandlerInterceptor operates inside Spring MVC, after the DispatcherServlet has resolved which handler (controller method) will process the request, giving it access to the handler itself and the resulting ModelAndView. Security enforcement belongs at the Filter level specifically because it should reject unauthorized requests before any MVC-layer processing (handler resolution, argument binding) happens at all.

CORS vs CSRF — distinct, often confused

CORS (Cross-Origin Resource Sharing) is a browser-enforced mechanism letting a server explicitly declare which origins are allowed to call it, via Access-Control-Allow-Origin and related response headers — it protects against a malicious site's JavaScript reading responses from an API the browser has credentials for. CSRF (Cross-Site Request Forgery) is about forged requests from an already-trusted origin (a malicious site tricking a logged-in user's browser into submitting a request the user never intended) — different threat model entirely, addressed with anti-forgery tokens, not origin headers.

API security checklist

Authentication and authorization enforced on every endpoint (no accidentally-open routes), input validation and output encoding, rate limiting (see the Rate Limiter case study), secrets never committed to source control, and least-privilege scoping for service-to-service calls — a practical baseline worth checking explicitly on any new API surface, not just relying on framework defaults to catch every gap.

Basic Security Configuration (Spring Boot 3)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())  // disable for REST APIs
            .sessionManagement(sm ->
                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()  // public endpoints
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()  // everything else needs auth
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public AuthenticationManager authManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

UserDetailsService

Spring Security loads user details through UserDetailsService:

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email)
            throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
            .map(user -> User.builder()
                .username(user.getEmail())
                .password(user.getPasswordHash())
                .roles(user.getRoles().toArray(new String[0]))
                .accountExpired(!user.isActive())
                .build())
            .orElseThrow(() ->
                new UsernameNotFoundException("User not found: " + email));
    }
}

SecurityContextHolder — accessing the current user

// Get the current authenticated user
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();

// In a controller — inject directly
@GetMapping("/me")
public UserDto getMe(@AuthenticationPrincipal UserDetails user) {
    return userService.findByEmail(user.getUsername());
}

Method-Level Security

@Configuration
@EnableMethodSecurity  // enables @PreAuthorize, @PostAuthorize
public class MethodSecurityConfig {}

@Service
public class AdminService {

    @PreAuthorize("hasRole('ADMIN')")
    public List<User> getAllUsers() { ... }

    @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
    public User getUser(String userId) { ... }

    @PostAuthorize("returnObject.ownerId == authentication.principal.id")
    public Document getDocument(String docId) { ... }
}

Interview Tips

  1. Explain the difference between authentication (verifying identity) and authorization (verifying permissions).
  2. STATELESS session management means no HttpSession is created — every request must carry credentials (JWT).
  3. @PreAuthorize uses Spring Expression Language (SpEL) — you can access the authentication object and method arguments.

Advertisement

How authentication actually happens inside the chain

The filter chain is easier to reason about once you know the objects that pass through it:

  1. An authentication filter (form login, HTTP Basic, the bearer-token filter, or your custom JWT filter) extracts credentials from the request, and builds an unauthenticated Authentication token, for example a UsernamePasswordAuthenticationToken holding a username and password.
  2. It hands the token to the AuthenticationManager, usually a ProviderManager, which asks each configured AuthenticationProvider in turn whether it supports that token type.
  3. The matching provider, for example DaoAuthenticationProvider, loads the user via UserDetailsService, checks the password with the PasswordEncoder, checks the account's status flags (locked, disabled, expired), and returns an authenticated Authentication containing the principal and the granted authorities.
  4. The filter stores that result in the SecurityContext (held by SecurityContextHolder, per thread). For session-based apps, it's also saved in the HTTP session, so later requests are recognized.
  5. Further down the chain, the AuthorizationFilter (and method security later) reads the authorities from the context, and grants or denies access.
  6. If anything throws AuthenticationException or AccessDeniedException, ExceptionTranslationFilter turns it into a 401 (unauthenticated: triggering the AuthenticationEntryPoint) or a 403 (authenticated but forbidden: the AccessDeniedHandler).

Supporting a new login method (an API key, a magic link, a partner's signed header) follows the same shape: a filter that builds a custom token type, and a provider that knows how to verify it. The rest of the framework keeps working unchanged.

Stateless API vs browser app configuration

The right defaults differ between these two cases, and mixing them is a common source of confusion:

ConcernBrowser app with sessionsStateless API with tokens
SessionIF_REQUIRED: the session stores the security contextSTATELESS: authenticate every request from its token
CSRF protectionEnabled. Cookies are sent automatically, so forged requests are possibleUsually disabled if credentials are only sent in the Authorization header, never in cookies
LoginForm login, or OAuth2 login with redirectsA token endpoint, or an external identity provider issuing JWTs
Unauthenticated responseRedirect to the login page401 with a JSON body, with no redirect

If a "stateless" API actually reads its token from a cookie, CSRF protection is needed again, because the browser attaches cookies automatically.

Follow-up questions this topic invites — and their answers

Q: What's the difference between a 401 and a 403 in Spring Security? A: 401 Unauthorized means the request isn't authenticated (missing or invalid credentials), and is handled by the AuthenticationEntryPoint. 403 Forbidden means the user is known but lacks permission, handled by the AccessDeniedHandler. ExceptionTranslationFilter decides which applies.

Q: What are AuthenticationManager and AuthenticationProvider? A: The manager is the entry point that authenticates a token. The usual implementation, ProviderManager, delegates to a list of providers, each supporting certain token types (username/password, JWT, LDAP and so on). The first provider that supports the token and succeeds returns the authenticated result.

Q: When is it safe to disable CSRF protection? A: When the application doesn't rely on credentials that browsers send automatically, such as cookies or HTTP Basic cached by the browser, for example a pure API authenticated by an Authorization: Bearer header. If any authentication travels in cookies, keep CSRF protection enabled.

Q: How does the SecurityContext reach my service code? A: It's stored in SecurityContextHolder, which uses a ThreadLocal by default, so code running on the request thread can read the current Authentication. It doesn't automatically propagate to @Async or other threads. Use DelegatingSecurityContextExecutor, or pass the needed identity explicitly.

Q: How do you customize the JSON returned for 401 and 403 errors in an API? A: Register a custom AuthenticationEntryPoint (for 401) and AccessDeniedHandler (for 403) in the security configuration's exceptionHandling() section, and write a consistent error body there. Controller-level @ControllerAdvice doesn't see these, because they happen in filters before the controller runs.

Advertisement

Next

JWT Authentication

AI Tutor

Lesson: Spring Security Overview

Quick actions

AI responses can be inaccurate. Verify critical information.