Spring Boot 9 min read

Spring Security Architecture: Complete Guide to How Security Works

Master Spring Security internals. Learn FilterChain, SecurityContext, Authentication flow, and how Spring Security protects your application.

MR

Moshiour Rahman

Advertisement

Why Understand Spring Security Architecture?

Most developers copy-paste security configurations without understanding how they work. This leads to security vulnerabilities and debugging nightmares. Understanding the architecture lets you:

  • Debug authentication issues quickly
  • Customize security for complex requirements
  • Avoid common security pitfalls
  • Build secure applications with confidence

The Big Picture

Spring Security is a filter-based framework. Every HTTP request passes through a chain of security filters before reaching your controller.

Spring Security Filter Chain Architecture

Core Components

1. SecurityFilterChain

The backbone of Spring Security. It’s an ordered list of filters that process every request.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}

2. SecurityContext & SecurityContextHolder

The SecurityContext holds the currently authenticated user. SecurityContextHolder provides access to it from anywhere in your application.

// Get current authenticated user anywhere in your code
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Object principal = authentication.getPrincipal();

// Check if user is authenticated
boolean isAuthenticated = authentication.isAuthenticated();

SecurityContext Storage Strategies

StrategyUse Case
MODE_THREADLOCALDefault. One context per thread.
MODE_INHERITABLETHREADLOCALChild threads inherit parent’s context
MODE_GLOBALSingle context for entire application
// Set strategy (usually in main class)
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

3. Authentication Object

The Authentication interface represents both the token for authentication request and the authenticated principal.

public interface Authentication extends Principal, Serializable {

    // Authorities granted to the principal
    Collection<? extends GrantedAuthority> getAuthorities();

    // Credentials (usually password, cleared after authentication)
    Object getCredentials();

    // Additional details (IP address, session ID, etc.)
    Object getDetails();

    // The principal (usually UserDetails)
    Object getPrincipal();

    // Whether the user is authenticated
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated);
}

Common Authentication Implementations

ClassUse Case
UsernamePasswordAuthenticationTokenForm login, basic auth
JwtAuthenticationTokenJWT-based authentication
OAuth2AuthenticationTokenOAuth2/OpenID Connect
AnonymousAuthenticationTokenUnauthenticated users
RememberMeAuthenticationTokenRemember-me functionality

4. AuthenticationManager

The API for authentication. It takes an Authentication request and returns a fully authenticated Authentication object.

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
        throws AuthenticationException;
}

5. AuthenticationProvider

The actual authentication logic. AuthenticationManager delegates to one or more AuthenticationProvider implementations.

public interface AuthenticationProvider {

    // Perform authentication
    Authentication authenticate(Authentication authentication)
        throws AuthenticationException;

    // Can this provider handle this authentication type?
    boolean supports(Class<?> authentication);
}

Custom AuthenticationProvider Example

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new BadCredentialsException("User not found"));

        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("Invalid password");
        }

        if (!user.isEnabled()) {
            throw new DisabledException("User is disabled");
        }

        if (user.isAccountLocked()) {
            throw new LockedException("Account is locked");
        }

        return new UsernamePasswordAuthenticationToken(
            user,
            null,
            user.getAuthorities()
        );
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication);
    }
}

6. UserDetailsService

Loads user-specific data. This is where you connect Spring Security to your user database.

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException;
}

Implementation Example

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        return userRepository.findByUsername(username)
            .orElseThrow(() ->
                new UsernameNotFoundException("User not found: " + username));
    }
}

7. UserDetails

Provides core user information required by Spring Security.

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

The Authentication Flow

Here’s exactly what happens when a user tries to authenticate:

1. User submits credentials (username/password)


2. Authentication Filter creates Authentication token
   (e.g., UsernamePasswordAuthenticationToken)


3. AuthenticationManager.authenticate() is called


4. AuthenticationManager iterates through AuthenticationProviders


5. Matching AuthenticationProvider processes the request
   ├── Loads UserDetails via UserDetailsService
   ├── Validates credentials (password check)
   └── Returns fully authenticated Authentication object


6. Authentication stored in SecurityContext
   (SecurityContextHolder.getContext().setAuthentication())


7. SecurityContextPersistenceFilter saves context
   (usually to HttpSession)


8. Request proceeds to controller

Key Security Filters

Filter Order (Default)

OrderFilterPurpose
1DisableEncodeUrlFilterPrevents session ID in URLs
2WebAsyncManagerIntegrationFilterAsync request support
3SecurityContextHolderFilterManages SecurityContext
4HeaderWriterFilterAdds security headers
5CorsFilterCORS handling
6CsrfFilterCSRF protection
7LogoutFilterLogout handling
8UsernamePasswordAuthenticationFilterForm login
9BasicAuthenticationFilterHTTP Basic auth
10RequestCacheAwareFilterCaches requests
11SecurityContextHolderAwareRequestFilterServlet API integration
12AnonymousAuthenticationFilterAnonymous user handling
13ExceptionTranslationFilterException handling
14AuthorizationFilterAccess control decisions

Adding Custom Filters

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Add custom filter BEFORE UsernamePasswordAuthenticationFilter
            .addFilterBefore(
                new CustomAuthFilter(),
                UsernamePasswordAuthenticationFilter.class
            )
            // Add custom filter AFTER BasicAuthenticationFilter
            .addFilterAfter(
                new AuditFilter(),
                BasicAuthenticationFilter.class
            )
            // Add custom filter AT specific position
            .addFilterAt(
                new CustomBasicAuthFilter(),
                BasicAuthenticationFilter.class
            );

        return http.build();
    }
}

Custom Filter Example

public class RequestLoggingFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        long startTime = System.currentTimeMillis();

        String requestId = UUID.randomUUID().toString();
        request.setAttribute("requestId", requestId);

        log.info("Request started: {} {} [{}]",
            request.getMethod(),
            request.getRequestURI(),
            requestId);

        try {
            filterChain.doFilter(request, response);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            log.info("Request completed: {} {} [{}] - {} in {}ms",
                request.getMethod(),
                request.getRequestURI(),
                requestId,
                response.getStatus(),
                duration);
        }
    }
}

GrantedAuthority & Roles

Authorities represent permissions. Roles are authorities with a ROLE_ prefix.

// Creating authorities
GrantedAuthority readAuthority = new SimpleGrantedAuthority("READ_PRIVILEGE");
GrantedAuthority adminRole = new SimpleGrantedAuthority("ROLE_ADMIN");

// In security config
http.authorizeHttpRequests(auth -> auth
    // Check for authority
    .requestMatchers("/read/**").hasAuthority("READ_PRIVILEGE")
    // Check for role (automatically prefixes with ROLE_)
    .requestMatchers("/admin/**").hasRole("ADMIN")
    // Multiple roles
    .requestMatchers("/manage/**").hasAnyRole("ADMIN", "MANAGER")
);

Role Hierarchy

@Bean
public RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("""
        ROLE_ADMIN > ROLE_MANAGER
        ROLE_MANAGER > ROLE_USER
        ROLE_USER > ROLE_GUEST
    """);
    return hierarchy;
}

@Bean
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(
        RoleHierarchy roleHierarchy) {
    DefaultMethodSecurityExpressionHandler handler =
        new DefaultMethodSecurityExpressionHandler();
    handler.setRoleHierarchy(roleHierarchy);
    return handler;
}

Exception Handling

Authentication Exceptions

ExceptionMeaning
BadCredentialsExceptionWrong username/password
DisabledExceptionAccount is disabled
LockedExceptionAccount is locked
AccountExpiredExceptionAccount has expired
CredentialsExpiredExceptionPassword has expired
UsernameNotFoundExceptionUser doesn’t exist

Custom Exception Handling

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .exceptionHandling(ex -> ex
                // Handle authentication failures
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType("application/json");
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.getWriter().write("""
                        {"error": "Unauthorized", "message": "%s"}
                        """.formatted(authException.getMessage()));
                })
                // Handle authorization failures
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType("application/json");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.getWriter().write("""
                        {"error": "Forbidden", "message": "Access denied"}
                        """);
                })
            );

        return http.build();
    }
}

Password Encoding

Never store passwords in plain text. Spring Security provides password encoders.

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

Password Encoder Comparison

EncoderStrengthUse Case
BCryptPasswordEncoderStrongRecommended default
Argon2PasswordEncoderVery StrongHigh security requirements
SCryptPasswordEncoderStrongMemory-hard hashing
Pbkdf2PasswordEncoderStrongFIPS compliance
NoOpPasswordEncoderNoneTesting only, never production

Delegating Password Encoder

Supports multiple encoders for migrating between algorithms:

@Bean
public PasswordEncoder passwordEncoder() {
    String defaultEncoder = "bcrypt";
    Map<String, PasswordEncoder> encoders = Map.of(
        "bcrypt", new BCryptPasswordEncoder(),
        "scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8(),
        "argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
    );

    return new DelegatingPasswordEncoder(defaultEncoder, encoders);
}

Stored passwords look like: {bcrypt}$2a$10$...

Complete Configuration Example

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;
    private final CustomAuthenticationEntryPoint authEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // Disable CSRF for APIs (enable for web apps with forms)
            .csrf(csrf -> csrf.disable())

            // CORS configuration
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))

            // Session management
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // Authorization rules
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )

            // Exception handling
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
            )

            // Authentication provider
            .authenticationProvider(authenticationProvider())

            // HTTP Basic (for testing)
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

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

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

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Debugging Tips

Enable Debug Logging

# application.yml
logging:
  level:
    org.springframework.security: DEBUG
    org.springframework.security.web.FilterChainProxy: DEBUG
@Component
public class SecurityFilterChainLogger implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private FilterChainProxy filterChainProxy;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        filterChainProxy.getFilterChains().forEach(chain -> {
            System.out.println("Filter Chain: " + chain);
            chain.getFilters().forEach(filter ->
                System.out.println("  - " + filter.getClass().getSimpleName())
            );
        });
    }
}

Summary

ComponentResponsibility
SecurityFilterChainOrdered list of security filters
SecurityContextHolds current authentication
AuthenticationRepresents authenticated user
AuthenticationManagerOrchestrates authentication
AuthenticationProviderPerforms actual authentication
UserDetailsServiceLoads user from database
PasswordEncoderHashes and verifies passwords
GrantedAuthorityRepresents permissions/roles

Understanding these components is essential for building secure Spring applications. In the next article, we’ll dive into database authentication and custom UserDetailsService implementations.

Series Navigation

  1. Spring Security Architecture (This Article)
  2. Database Authentication & UserDetailsService
  3. OAuth2 & Social Login
  4. Method-Level Security
  5. CSRF, CORS & Security Headers
  6. Production Security Best Practices

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.