In the previous lesson, we explored the theory behind JSON Web Tokens, understanding why their stateless nature is ideal for modern APIs. We covered the structure of a JWT, the critical role of the signature, and the access/refresh token pattern for balancing security and user experience. Now, it's time to transition from theory to practice and implement these concepts in our Spring Boot application.
This lesson will guide you through configuring Spring Security, the framework that will protect our REST API. You will learn how to override Spring's default security settings to create a custom configuration tailored for stateless, JWT-based authentication. We will set up the essential components that instruct Spring Security on how to handle incoming requests, paving the way for us to secure our endpoints.
From Default Security to Custom Configuration
When you add a Spring Security dependency to your project, it adopts a "secure by default" posture. Any attempt to access an endpoint will be met with an authentication challenge. While excellent for preventing accidental exposure, this default behavior—which often involves login forms and server-side sessions—is not what we need for a stateless REST API.
Our goal is to create a custom configuration that explicitly tells Spring Security to:
- Not manage user sessions (be stateless).
- Expect a JWT in the
Authorizationheader of incoming requests. - Know how to validate that JWT to confirm the user's identity.
To do this, we create a special configuration class.
#32 Spring Security | Custom Configuration
The video "#32 Spring Security | Custom Configuration" from Telusko provides a great conceptual start. It explains why a custom configuration is necessary and introduces the core components we'll be using.
Watch the introduction to understand the need for a custom configuration and the purpose of the @Configuration and @EnableWebSecurity annotations. Next, focus on how we define our own security rules using a SecurityFilterChain bean. This bean is the heart of modern Spring Security configuration.
As the video explains, we define a Java class annotated with @Configuration and @EnableWebSecurity. Inside this class, we declare a Bean of type SecurityFilterChain. This bean is a method that takes an HttpSecurity object as a parameter and returns the configured result. By defining this bean, we take full control from Spring's default setup.
Building the Security Filter Chain
The HttpSecurity object is our main tool. We use a builder pattern to chain together configuration methods, defining a pipeline of security rules. Here is the typical structure for a stateless API:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Step 1: Disable CSRF
.csrf(csrf -> csrf.disable())
// Step 2: Configure session management to be stateless
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Step 3: Define authorization rules
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated() // For now, all requests require authentication
)
// Step 4: Configure JWT support
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}
Let's break down each part of this chain.
1. Disabling CSRF (Cross-Site Request Forgery)
CSRF is an attack that tricks a user's browser into making an unwanted request to a site where they are already authenticated (e.g., via a session cookie). Spring Security's CSRF protection works by requiring a unique token with each state-changing request.
However, in a stateless API that uses JWTs, the client is responsible for acquiring and sending the token with every request, usually in a header. This mechanism itself prevents CSRF, so the built-in protection is not needed and can be safely disabled.
2. Stateless Session Management
This is the most critical line for achieving our stateless architecture.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
This command tells Spring Security not to create or use any HttpSession. Each request must be authenticated independently based on the information it carries (i.e., the JWT), perfectly aligning with the principles we discussed in the last lesson.
3. Defining Authorization Rules
The authorizeHttpRequests block defines which endpoints are public and which are protected. For now, we are keeping it simple: anyRequest().authenticated(). This rule means that every single request to our API, regardless of the path, must be authenticated. In a later lesson, we will modify this to permit unauthenticated access to our login and registration endpoints.
4. Configuring JWT Support
The line .oauth2ResourceServer(oauth2 -> oauth2.jwt(...)) is where we integrate JWT validation. This configures our application as an OAuth2 Resource Server, which is the standard way to protect an API that accepts bearer tokens. By specifying .jwt(), we are telling it that the bearer tokens will be JWTs.
When we add this configuration, Spring Security knows it needs to look for a JWT in the Authorization: Bearer <token> header. However, it still doesn't know how to validate the token's signature. If you run the application with this configuration, it will fail to start, complaining that it can't find a JwtDecoder bean.
Providing the JWT Decoder
To validate a JWT's signature, the server needs the correct key. As discussed previously, the recommended approach is asymmetric encryption using an RSA key pair (a private key for signing and a public key for verification).
Our Spring Security configuration needs access to the public key to create a JwtDecoder. The decoder's job is to parse the incoming token and verify its signature using that public key.
The following video from Dan Vega provides a complete, practical walkthrough of this entire process, from setting up the project to configuring the decoder.
Spring Security JWT: How to secure your Spring Boot REST APIs with JSON Web Tokens
This video, "Spring Security JWT," is a fantastic, hands-on guide. It builds the exact configuration we need. We'll use it to understand the practical steps involved.
First, watch the initial setup of the SecurityFilterChain. Dan configures CSRF, authorization, and stateless session management, similar to what we've just discussed. He uses httpBasic initially for testing, but we will jump straight to the JWT configuration. Next, see how he enables JWT support with oauth2ResourceServer. Pay close attention to the error he gets when he restarts the application—it highlights the missing JwtDecoder bean, which is exactly the problem we need to solve. Now, watch the crucial section on generating RSA keys using openssl. You will need to run these commands in a terminal to generate your own public and private keys. Place them in src/main/resources/certs. The commands are: openssl genrsa -out keypair.pem 2048 (Generates the initial key) openssl rsa -in keypair.pem -pubout -out public.pem (Extracts the public key) openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.pem (Converts the private key to the required PKCS8 format) You can then delete the initial keypair.pem file. Finally, watch how he uses these keys to create the JwtDecoder. He first creates a ConfigurationProperties record to load the key paths from application.properties, which is excellent practice. Then, he defines the JwtDecoder bean using NimbusJwtDecoder.withPublicKey(), passing in the public key.
The Complete SecurityConfig
After following the steps in the video, your project structure will include the generated keys and updated properties.
src/main/resources/application.properties:
rsa.public-key=classpath:certs/public.pem
rsa.private-key=classpath:certs/private.pem
You'll have a new record to hold these properties:RsaKeyProperties.java:
@ConfigurationProperties(prefix = "rsa")
public record RsaKeyProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
}
And your final SecurityConfig class will look like this, bringing everything together. Notice how we inject the RsaKeyProperties and use them to build the decoder and encoder beans (we'll use the encoder in the next lesson).
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final RsaKeyProperties rsaKeys;
public SecurityConfig(RsaKeyProperties rsaKeys) {
this.rsaKeys = rsaKeys;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
}
Note: We have also included a JwtEncoder bean. While the JwtDecoder is used for validating incoming tokens, the JwtEncoder will be used in our next lesson to create and sign new tokens during login.
Conclusion
Congratulations! You have successfully configured Spring Security for a stateless, JWT-based REST API. This is a foundational and highly marketable skill for a backend developer. With this setup in place, our application is ready to validate JWTs, but it has no way to issue them yet. Any request sent to it will result in a 401 Unauthorized error because we can't provide a valid token.
Key Takeaways:
- A custom
SecurityFilterChainbean is used to override Spring Security's defaults. - For a stateless API, you must disable CSRF protection and set the session creation policy to
STATELESS. .oauth2ResourceServer().jwt()configures the application to act as a resource server that validates JWT bearer tokens.- This configuration requires a
JwtDecoderbean, which needs a public key to verify the token's signature. - Asymmetric RSA keys (public/private pair) are the recommended standard for signing JWTs, and you can generate them using
openssl.
In the next lesson, we will build upon this foundation. We will implement the user registration and login endpoints. At the login step, we will use the JwtEncoder we defined today to generate a signed JWT and send it back to the client, finally completing our authentication loop.