While building any application, the first and most important thing is that it should work properly. The next thing that comes into the picture is that it should be secure and protected.
Spring security is a powerful framework for securing Spring-based applications. It offers robust support for authentication, authorization, and protection against common attacks like CSRF.
In this article, we will dive into each component of spring security and understand their roles and flow in detail.
Spring Security Architecture
Components of Architecture
Security Filter Chain
The security filter chain is a series of filters that process incoming HTTP requests in the order they are declared. These filters are the backbone of Spring Security and handle tasks like authentication, authorization, and session management.
Key Filters:
UsernamePasswordAuthenticationFilter
: Handles form-based login.BasicAuthenticationFilter
: Processes HTTP Basic Authentication.CsrfFilter
: Manages CSRF protection.FilterSecurityInterceptor
: Performs access control checks.
Configuration:
In spring Security we create a separate SecurityConfig
class annotated with @Configuration to provide custom implementations of various components.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity , JwtAuthFilter jwtAuthFilter) throws Exception {
return httpSecurity
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/v1/login","/auth/v1/signup","/auth/v1/refreshToken").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(ses -> ses.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(Customizer.withDefaults())
.formLogin(customizer -> customizer.disable())
//.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) -> this way we can add custom filter in the filter chain
.build();
}
Authentication Manager
The Authentication Manager
is the central component that verifies user identities. It coordinates the authentication process and delegates the actual authentication to Authentication Providers
.
In more complex systems, you might have multiple Authentication Managers. Each manager can be associated with a specific set of Authentication Providers, allowing you to handle different types of authentication for different parts of your application.
How it works:
Takes an
Authentication
object (credentials, principal).Passes it to an appropriate
AuthenticationProvider
.Returns a fully authenticated
Authentication
object if successful.
Configuration:
We can define a custom AuthenticationManager
in our SecurityConfig
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
Authentication Provider
The Authentication Providers
are the workers of the Authentication Manager
. They are responsible for actually performing the authentication.
It is a strategy for authentication and can support multiple providers (eg. database , LDAP)
How it works:
Checks if it supports the given authentication type.
Retrieves user details (via
UserDetailsService
).Verifies credentials (e.g., password).
Returns a valid
Authentication
object or throws an exception.
Spring Security supports various types of Authentication Providers
DaoAuthenticationProvider
— uses aUserDetailsService
to retrieve user details from database and compare credentials.LdapAuthenticationProvider
— used for authenticating against LDAP servers.
Example:
@Bean
public AuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(myUserDetailsService);
return provider;
}
- In this snippet, a custom
DaoAuthenticationProvider
is defined, configured with aUserDetailsService
and aPasswordEncoder
.
Spring’s Default Behaviour:
If no custom AuthenticationManager
is defined, Spring Security automatically creates one and registers all available AuthenticationProvider
s.
You don't need to define a custom AuthenticationManager
if:
You rely on Spring's default
ProviderManager
to aggregate multipleAuthenticationProvider
s.You only want to define and plug in custom logic through an
AuthenticationProvider
.Example: In the above example we don’t need to define custom AuthenticationManager.
You should define your own AuthenticationManager
if:
You need full control over how providers are registered and chained.
You want to explicitly configure how the
AuthenticationManager
interacts with one or moreAuthenticationProvider
s.Example: Custom AuthenticationManager with multiple providers.
@Bean public AuthenticationManager customAuthenticationManager() { DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider(); daoProvider.setUserDetailsService(customUserDetailsService); daoProvider.setPasswordEncoder(passwordEncoder()); CustomAuthenticationProvider customProvider = new CustomAuthenticationProvider(); return new ProviderManager(Arrays.asList(daoProvider, customProvider)); }
Now you must be thinking about
how the auth manager chooses which provider to use. The answer is in Spring Security, when multiple authentication providers are configured, the
AuthenticationManager
chooses which one to use based on the order they are defined. By default, it tries each provider in the order they are listed in theAuthenticationManager
configuration, stopping as soon as one successfully authenticates the user.
Password Encoder:
The PasswordEncoder
handles password hashing and verification . Storing plain-text passwords is insecure.
BCryptPasswordEncoder
- this is widely recommended choice for securely hashing passwords in Spring SecurityStandardPasswordEncoder
- this encoder uses one-way hashing algorithm which is less secure and it is not recommended.MessageDigestPasswordEncoder
- this encoder uses a specified message digest algorithm (e.g., SHA-256) to hash passwords. While it’s more secure than plain text, it’s not as strong as BCrypt and is considered less secure in modern applications.SCryptPasswordEncoder
- SCrypt is another secure password hashing algorithm, similar to BCrypt. It’s designed to be memory-intensive, making it resistant to certain types of attacks. SCryptPasswordEncoder is a good choice for secure password hashing.
Example:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // here 12 is the strength or rounds of hashing .
// the default strength is 10 and it can lie between 0 to 20
// setting high strength can lead to delay in the process
}
UserDetailsService
The UserDetailsService
is a core interface for loading user-specific data. It is used by the AuthenticationProvider
to retrieve user details from a database or another source.
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
public UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> user = userRepository.findById(username);
if (user.isEmpty()) {
throw new UsernameNotFoundException("user not found with this username");
}
return new UserDetailsImpl(user.get());
}
}
- The
UserDetailsServiceImpl
class is implemented as a customUserDetailsService
, and it overrides theloadUserByUsername
method to retrieve user details from a database via aUserRepository
.
Security Context Holder
The SecurityContextHolder
is the foundation of Spring Security's thread-based security model. It's used to store and retrieve the SecurityContext
, which contains authentication and possibly other security-related details.
How it Works:
When a user successfully authenticates, the resulting
Authentication
object is stored in theSecurityContext
.The
SecurityContextHolder
uses thread-local storage to keep the security context tied to the current thread.Once the request is complete, Spring Security clears the
SecurityContextHolder
to avoid data leaks.
Key Methods:
// Set Authentication
Authentication authentication = new UsernamePasswordAuthenticationToken(user, password, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Get Authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
// Clear Context
SecurityContextHolder.clearContext();
Common Issues:
Data Not Persisting Across Threads:
By default,
SecurityContextHolder
usesThreadLocal
storage. If the execution spans multiple threads (e.g., asynchronous processing), the security context won't carry over.Solution: Use
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
to propagate the context to child threads:SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
Null Authentication:
This occurs if the security context is not set (e.g., during unauthenticated access or manual testing).
Solution: Ensure authentication is set programmatically in tests or correctly configured in the filter chain.
Best Practices:
It is not recommended to access it directly in business logic instead we can use @AuthenticationPrincipal annotation in the controller instead.
@GetMapping("/profile") public ResponseEntity<?> getProfile(@AuthenticationPrincipal UserDetails user) { return ResponseEntity.ok(user); }
Principal :
The Principal in Spring Security refers to the currently authenticated user.t represents the identity of the user interacting with the application and is stored in the SecurityContext
.
It is part of Authentication
object stored in SecurityContext
. It can be any object that represents the authenticated user, often an instance of UserDetails
or a custom implemetation of it.
Accessing the Principal:
a. Using SecurityContextHolder
@GetMapping("/profile")
public ResponseEntity<?> getProfile() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
return ResponseEntity.ok("Logged in user: " + username);
}
return ResponseEntity.ok("Anonymous user");
}
b. Using @AuthenticationPrincipal
@GetMapping("/profile")
public ResponseEntity<?> getProfile(@AuthenticationPrincipal UserDetails user) {
return ResponseEntity.ok("Logged in user: " + user.getUsername());
}
Here’s how the principal flows through the authentication process:
Login Attempt:
- The user submits their credentials (e.g., username and password).
AuthenticationManager Delegation:
- The
AuthenticationManager
delegates authentication to anAuthenticationProvider
.
- The
UserDetails Loaded:
The
UserDetailsService
loads the user's details (username, password, roles).The
UserDetails
instance is set as theprincipal
.
SecurityContext Updated:
- If authentication is successful, the
Authentication
object is stored in theSecurityContext
.
- If authentication is successful, the
Accessing Principal:
- Throughout the session, the principal can be accessed via the
SecurityContext
.
- Throughout the session, the principal can be accessed via the
Working Flow
Now let us combine all our learnings and understand the whole flow of architecture.
The request is intercepted by the
Security Filter Chain
. The Security Filter Chain consists of a series of filters, each with a specific security-related task.If the user is not yet authenticated (i.e., not logged in), Spring Security’s authentication filters will trigger the
Authentication Manager
. If the credentials match, the Authentication Manager generates anAuthentication Object
indicating a successful authentication.The
Authentication Manager
uses the configuredAuthentication Providers
to verify the user’s credentials.Authentication Providers
will use thePasswordEncoder
to store and compare passwords.Authentication Providers
may use theUserDetailsService
to fetch user details. The user’s credentials are compared to the stored or provided credentials.UserDetailsService
will fetch the data from the database.The status of the authentication process will be sent to the user as a success or unauthorized response.
This Authentication object is stored within the security context managed by
SecurityContextHolder
. The security context now represents the authenticated user.
Conclusion
I hope this article has helped you understand the key components and the feel of spring security.
Following this article I will publish another article in which we will see how can we implement JWT-based authentication in our spring boot application do check it out and subscribe to my newsletter so that we can learn together.
Thank you for reading and Happy Coding