Hello! In our last lesson, we explored the "what" and "why" of the Spring Security filter chain architecture. We learned that every request to a secured application passes through an ordered series of filters, each performing a specific security function.
Today, we move from theory to practice. This lesson is all about the "how." We will take control of the security process by defining our own SecurityFilterChain to enforce custom rules.
Our goal for this lesson is to configure basic authentication and URL-based authorization rules using a SecurityFilterChain. By the end, you'll be able to create a configuration that makes some API endpoints public while protecting others, requiring clients to provide credentials to access them.
1. Defining Your Custom Security Configuration
In modern Spring Security (version 5.7 and newer), the standard way to define security rules is by creating a @Bean of type SecurityFilterChain inside a configuration class. This component-based approach is flexible and has replaced the older WebSecurityConfigurerAdapter pattern.
Let's start by setting up the class that will hold our security configuration.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Our security rules will go here
return http.build();
}
}
There are two key annotations here:
@Configuration: This is a standard Spring annotation that marks the class as a source of bean definitions.@EnableWebSecurity: This is the master switch that enables Spring Security's web security support and provides integration with Spring MVC.
The securityFilterChain method is where the magic happens. Spring injects an HttpSecurity object, which acts as a builder for our security rules. We use its fluent API to define our configuration and finally call http.build() to create the SecurityFilterChain instance.
2. URL-Based Authorization: Who Can Access What?
The first thing we'll configure is authorization: the rules that determine whether a user is allowed to perform an action. A common pattern is to grant or deny access based on the URL of the incoming request.
Let's say we want our API to have two types of endpoints:
/api/public/**: Accessible by anyone./api/private/**: Accessible only by authenticated users.
We can configure this using the authorizeHttpRequests() method.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll() // (1)
.anyRequest().authenticated() // (2)
);
return http.build();
}
}
Let's break this down:
.requestMatchers("/api/public/**").permitAll(): We userequestMatcherswith an Ant-style path pattern to select all endpoints under/api/public/. The.permitAll()rule specifies that these requests do not require any authentication..anyRequest().authenticated(): This is a catch-all rule. It specifies that any request that hasn't been matched yet requires the user to be authenticated. This is a crucial security practice known as "deny by default." We explicitly open up public endpoints and secure everything else by default.
3. Enabling an Authentication Method: HTTP Basic
We've defined that some requests require authentication, but we haven't told Spring Security how to authenticate users. For REST APIs, the default form-based login is not suitable. A simpler mechanism for server-to-server or programmatic clients is HTTP Basic Authentication.
With Basic Auth, the client sends a standard HTTP header Authorization: Basic <credentials>, where <credentials> is a Base64-encoded string of username:password.
Let's enable Basic Auth and also configure two other important settings for REST APIs: disabling Cross-Site Request Forgery (CSRF) protection and ensuring stateless session management.
Spring Security Authentication
The following article from the official Spring documentation provides a clear guide on configuring security. We will focus on the modern, component-based approach for defining our security rules.
Please read the following sections to understand the key parts of our configuration: Start with Section 2.2.2 Component-based Approach. This confirms the @Bean factory method approach we are using. Read Section 2.8 Authorize Requests. This explains the .permitAll() and .authenticated() rules we just discussed. Read Section 2.9 Authentication. This shows how to enable Basic Authentication using .httpBasic(). Finally, read Section 2.11 Stateless Session Configuration. This explains how to configure stateless sessions, which is essential for scalable microservices.
Based on that reading, let's update our configuration. We will enable httpBasic, disable csrf, and set the session creation policy to STATELESS.
import static org.springframework.security.config.Customizer.withDefaults;
// ... inside SecurityConfig class
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable()) // (1)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // (2)
.httpBasic(withDefaults()); // (3)
return http.build();
}
- CSRF Disabled: CSRF is a protection mechanism for browser-based clients using sessions. Since we are building a stateless REST API that will be consumed by non-browser clients, this protection is not necessary and can be disabled.
- Stateless Session: This tells Spring Security not to create an
HttpSession. Each request must be authenticated independently, which is a core tenet of stateless architectures and vital for building scalable microservices. - HTTP Basic Enabled:
.httpBasic(withDefaults())adds theBasicAuthenticationFilterto our security filter chain. This filter is responsible for looking for theAuthorizationheader, decoding the credentials, and attempting to authenticate the user.
4. Providing User Credentials
We've told Spring Security to use Basic Auth, but where does it get the valid usernames and passwords to check against? This is the job of the UserDetailsService.
For this lesson, we'll use a simple in-memory user store. This is great for development and testing, as it doesn't require setting up a database. We also need to define a PasswordEncoder, because passwords must never be stored in plain text. The standard and recommended encoder is BCryptPasswordEncoder.
Let's add beans for both UserDetailsService and PasswordEncoder to our SecurityConfig.
Getting Started | Securing a Web Application
The official Spring guide on securing a web application provides a perfect example of how to set up an in-memory user store. Although the guide uses form login, the UserDetailsService and PasswordEncoder beans are configured exactly as we need them.
Read the section titled 'Set up Spring Security', focusing on the WebSecurityConfig class. Pay close attention to the passwordEncoder() and userDetailsService() bean methods. Notice how an in-memory user is created with a username, an encoded password, and roles.
Here is the complete SecurityConfig class incorporating all the concepts:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("password123"))
.roles("ADMIN", "USER")
.build();
UserDetails user = User.withUsername("user")
.password(encoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
Now, when a request comes in with Basic Auth credentials, the BasicAuthenticationFilter will use our UserDetailsService to look up the user by username. It will then use the PasswordEncoder to compare the provided password with the stored hashed password.
5. Testing Your Configuration
With this configuration in place, let's test our endpoints. You can use a command-line tool like curl.
1. Accessing a public endpoint (should succeed):
$ curl http://localhost:8080/api/public/hello
You should receive a 200 OK response.
2. Accessing a private endpoint without credentials (should fail):
$ curl -v http://localhost:8080/api/private/data
You should receive a 401 Unauthorized response. The -v flag will show you the WWW-Authenticate: Basic realm="..." header, which is the server's challenge for credentials.
3. Accessing a private endpoint with correct credentials (should succeed):
$ curl -u user:password http://localhost:8080/api/private/data
You should now receive a 200 OK response, as you've provided valid credentials.
6. The Filter Chain Revisited
Let's connect this back to what we learned in the last lesson. Our HttpSecurity configuration is a high-level DSL that assembles the SecurityFilterChain.
.httpBasic()added theBasicAuthenticationFilter..authorizeHttpRequests()configured theAuthorizationFilterto enforce our URL-based rules.
This video from a Spring Security engineer provides a great walkthrough of the modern configuration style and how it relates to the underlying components.
Spring Security Architecture Principles by Daniel Garnier-Moiroux @ Spring I/O 2024
This clip from Daniel Garnier-Moiroux's talk at Spring I/O demonstrates how the modern configuration syntax is used to build the filter chain and define security rules.
Watch from 06:20 to 08:31. The speaker shows how to define a SecurityFilterChain bean and configure authorization rules (anyRequest().authenticated() vs. permitAll()). This directly corresponds to the configuration we just wrote and reinforces the connection between the code and the security architecture.
Understanding that this concise configuration builds a powerful, underlying filter chain is key. When you need to debug, you can always fall back to the TRACE logging technique from our previous lesson to see exactly which filters are running and what decisions they are making.
Conclusion
In this lesson, you've made the leap from theory to practice. You now have the fundamental skills to secure a Spring Boot application.
Key Takeaways:
- Custom security rules are defined in a
SecurityFilterChainbean within a@Configurationclass. - Authorization rules are set using
authorizeHttpRequests(), where you can match request URLs and apply permissions likepermitAll()orauthenticated(). - Authentication methods are enabled via the
HttpSecuritybuilder; we used.httpBasic()for our API. - For REST APIs, it's best practice to disable CSRF (
.csrf().disable()) and use stateless sessions (SessionCreationPolicy.STATELESS). - Spring Security needs a
UserDetailsServiceto load user data and aPasswordEncoderto securely verify passwords. We used anInMemoryUserDetailsManagerandBCryptPasswordEncoderfor a simple setup.
While HTTP Basic is simple, it requires sending the username and password with every request, which is not always ideal. In our next lesson, we will explore a more modern and powerful authentication mechanism for microservices: JSON Web Tokens (JWT).