How Does Spring Security Work? A Deep Dive Into Its Mechanisms

How Does Spring Security Work? A Deep Dive Into Its Mechanisms

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 a UserDetailsService 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 a UserDetailsService and a PasswordEncoder.

Spring’s Default Behaviour:

If no custom AuthenticationManager is defined, Spring Security automatically creates one and registers all available AuthenticationProviders.

You don't need to define a custom AuthenticationManager if:

  • You rely on Spring's default ProviderManager to aggregate multiple AuthenticationProviders.

  • 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 more AuthenticationProviders.

  • 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 the AuthenticationManager 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 Security

  • StandardPasswordEncoder - 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 custom UserDetailsService, and it overrides the loadUserByUsername method to retrieve user details from a database via a UserRepository.

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 the SecurityContext.

  • 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 uses ThreadLocal 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:

  1. Login Attempt:

    • The user submits their credentials (e.g., username and password).
  2. AuthenticationManager Delegation:

    • The AuthenticationManager delegates authentication to an AuthenticationProvider.
  3. UserDetails Loaded:

    • The UserDetailsService loads the user's details (username, password, roles).

    • The UserDetails instance is set as the principal.

  4. SecurityContext Updated:

    • If authentication is successful, the Authentication object is stored in the SecurityContext.
  5. Accessing Principal:

    • Throughout the session, the principal can be accessed via the SecurityContext.

Working Flow

Now let us combine all our learnings and understand the whole flow of architecture.

  1. 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.

  2. 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 an Authentication Object indicating a successful authentication.

  3. The Authentication Manager uses the configured Authentication Providers to verify the user’s credentials.

  4. Authentication Providers will use the PasswordEncoder to store and compare passwords.

  5. Authentication Providers may use the UserDetailsService to fetch user details. The user’s credentials are compared to the stored or provided credentials.

  6. UserDetailsService will fetch the data from the database.

  7. The status of the authentication process will be sent to the user as a success or unauthorized response.

  8. 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

GitHub

LinkedIn

Hashnode