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.
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.
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
| Strategy | Use Case |
|---|---|
MODE_THREADLOCAL | Default. One context per thread. |
MODE_INHERITABLETHREADLOCAL | Child threads inherit parent’s context |
MODE_GLOBAL | Single 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
| Class | Use Case |
|---|---|
UsernamePasswordAuthenticationToken | Form login, basic auth |
JwtAuthenticationToken | JWT-based authentication |
OAuth2AuthenticationToken | OAuth2/OpenID Connect |
AnonymousAuthenticationToken | Unauthenticated users |
RememberMeAuthenticationToken | Remember-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)
| Order | Filter | Purpose |
|---|---|---|
| 1 | DisableEncodeUrlFilter | Prevents session ID in URLs |
| 2 | WebAsyncManagerIntegrationFilter | Async request support |
| 3 | SecurityContextHolderFilter | Manages SecurityContext |
| 4 | HeaderWriterFilter | Adds security headers |
| 5 | CorsFilter | CORS handling |
| 6 | CsrfFilter | CSRF protection |
| 7 | LogoutFilter | Logout handling |
| 8 | UsernamePasswordAuthenticationFilter | Form login |
| 9 | BasicAuthenticationFilter | HTTP Basic auth |
| 10 | RequestCacheAwareFilter | Caches requests |
| 11 | SecurityContextHolderAwareRequestFilter | Servlet API integration |
| 12 | AnonymousAuthenticationFilter | Anonymous user handling |
| 13 | ExceptionTranslationFilter | Exception handling |
| 14 | AuthorizationFilter | Access 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
| Exception | Meaning |
|---|---|
BadCredentialsException | Wrong username/password |
DisabledException | Account is disabled |
LockedException | Account is locked |
AccountExpiredException | Account has expired |
CredentialsExpiredException | Password has expired |
UsernameNotFoundException | User 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
| Encoder | Strength | Use Case |
|---|---|---|
BCryptPasswordEncoder | Strong | Recommended default |
Argon2PasswordEncoder | Very Strong | High security requirements |
SCryptPasswordEncoder | Strong | Memory-hard hashing |
Pbkdf2PasswordEncoder | Strong | FIPS compliance |
NoOpPasswordEncoder | None | Testing 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
Print Filter Chain
@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
| Component | Responsibility |
|---|---|
SecurityFilterChain | Ordered list of security filters |
SecurityContext | Holds current authentication |
Authentication | Represents authenticated user |
AuthenticationManager | Orchestrates authentication |
AuthenticationProvider | Performs actual authentication |
UserDetailsService | Loads user from database |
PasswordEncoder | Hashes and verifies passwords |
GrantedAuthority | Represents 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
- Spring Security Architecture (This Article)
- Database Authentication & UserDetailsService
- OAuth2 & Social Login
- Method-Level Security
- CSRF, CORS & Security Headers
- Production Security Best Practices
Advertisement
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
Spring Security Database Authentication: Custom UserDetailsService Guide
Implement database authentication in Spring Security. Learn UserDetailsService, password encoding, account locking, and multi-tenant authentication.
Spring BootSpring Boot JWT Authentication: Complete Security Guide
Implement JWT authentication in Spring Boot 3. Learn Spring Security 6, token generation, validation, refresh tokens, and role-based access control.
Spring BootSpring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.