Hello! In our last lesson, we built a login endpoint that authenticates users and issues a JSON Web Token (JWT). We now have a secure way of verifying a user's identity and giving them a "digital key." However, this key doesn't unlock any doors yet. Our API endpoints are still protected, but they don't know how to recognize or validate this JWT.
Today, we will complete the authentication loop by creating a JWT validation filter. This filter will act as a gatekeeper for our protected API endpoints. It will intercept every incoming request, look for a JWT in the Authorization header, and if a valid token is found, it will establish the user's identity for that request. This is the crucial step that transforms our token from a simple string into a functioning access pass.
By the end of this lesson, you will have a fully secured API where access to protected resources is granted only to requests carrying a valid JWT.
The Role of Filters in Spring Security
Before we write any code, let's understand the core concept. Spring Security protects your application by using a series of filters, known as the SecurityFilterChain. Think of it as an assembly line of security checks. When an HTTP request comes in, it passes through this chain, and each filter has the opportunity to inspect or modify the request before it reaches your controller.
Our goal is to add a custom filter to this chain. Its specific job will be to handle JWT authentication. We'll build our filter by extending OncePerRequestFilter, a convenient base class from Spring that guarantees our filter logic is executed exactly once for every request.
Implementing the JWT Authentication Filter
Let's create the filter. This single class will contain the entire logic for validating a token and authenticating the user for a given request.
Spring Boot Security - JWT Filter
The video "Spring Boot Security - JWT Filter" provides a great walkthrough for creating the filter. We will follow its structure but adapt it to our existing codebase, which uses constructor injection.
Watch the initial setup from this section to understand the creation of the JWTAuthenticationFilter class extending OncePerRequestFilter. Then, observe how the token is extracted from the request header in the clip from this part. We'll implement this logic next.
Create a new package named config or security, and inside it, create a new class JwtAuthenticationFilter.java.
package com.example.yourproject.config;
import com.example.yourproject.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7); // "Bearer ".length() is 7
// TODO: Extract username from token
// TODO: Validate token and update SecurityContext
}
}
Let's break down this initial structure:
- We inject
JwtService(which we'll complete shortly) andUserDetailsService(which we created in the last lesson). - The core logic is in
doFilterInternal. - We first check the
Authorizationheader. If it's missing or doesn't start withBearer, we simply pass the request to the next filter in the chain and stop processing. This is crucial for allowing access to public endpoints like/auth/login. - If a bearer token is present, we extract the JWT string itself by removing the "Bearer " prefix.
Completing the JwtService for Validation
In the last lesson, our JwtService had placeholder methods for validation. Now it's time to implement them. These methods will use the jjwt library to parse the token and extract its information, known as "claims".
Open your JwtService.java and add the following methods.
// In JwtService.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
// ... other imports
@Service
public class JwtService {
// ... existing SECRET_KEY, JWT_EXPIRATION, and generateToken methods
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// getSignInKey() and getJwtExpiration() from previous lesson
}
Here's what these new methods do:
extractAllClaims: This is the private helper that does the heavy lifting. It parses the JWT using our secret key and returns all the claims contained within it.extractClaim: A generic helper that allows us to extract a specific claim (like subject or expiration) from the claims body.extractUsername: UsesextractClaimto get the "subject" claim, which holds the username.isTokenValid: This is our main validation method. It performs two checks:- Does the username in the token match the user details loaded from the database?
- Is the token expired?
Updating the Security Context
Now we can complete our JwtAuthenticationFilter. Once we've validated the token, we need to tell Spring Security who the current user is. We do this by updating the SecurityContextHolder.
Spring Boot Security - JWT Filter
The video demonstrates this exact process.
Watch how the presenter validates the token and extracts the username. Then, see how a UsernamePasswordAuthenticationToken is created and set in the security context from this crucial segment.
Let's add this logic to the doFilterInternal method in JwtAuthenticationFilter.java.
// In JwtAuthenticationFilter.java, inside doFilterInternal method
final String jwt = authHeader.substring(7);
final String username = jwtService.extractUsername(jwt);
// Check if user exists and is not already authenticated
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null, // Credentials are not needed as we use JWT
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// Update SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
// Always pass the request to the next filter
filterChain.doFilter(request, response);
Let's analyze the new code block:
- We extract the username from the token.
- The
ifcondition checks that we have a username and that the user is not already authenticated. This prevents redundant checks. - We use our
UserDetailsServiceto load theUserobject from the database. - If the token is valid for this user, we create a
UsernamePasswordAuthenticationToken. This object is what Spring Security uses internally to represent the currently authenticated user. - We set this
authTokenin theSecurityContextHolder. This is the key step. From this point forward, for the duration of this request, Spring Security knows the user is authenticated. - Finally, we call
filterChain.doFilterto continue the request processing.
Adding the Filter to the Security Chain
Our filter is built, but Spring Security doesn't know about it yet. We need to explicitly add it to the SecurityFilterChain.
Spring Boot Security - JWT Filter
This part of the video explains how to add the custom filter to the configuration.
Watch from this section to see how addFilterBefore is used to place our custom filter in the correct position in the chain.
Open your SecurityConfig.java file and modify the securityFilterChain bean.
// In SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthenticationProvider authenticationProvider;
private final JwtAuthenticationFilter jwtAuthFilter; // Inject our filter
public SecurityConfig(
AuthenticationProvider authenticationProvider,
JwtAuthenticationFilter jwtAuthFilter // Add to constructor
) {
this.authenticationProvider = authenticationProvider;
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // Allow auth endpoints
.anyRequest().authenticated() // Secure all other endpoints
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider) // Set our custom provider
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Add our filter
return http.build();
}
}
The critical line is .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class). This tells Spring Security to run our jwtAuthFilter before its standard UsernamePasswordAuthenticationFilter. This is because we are handling authentication with a token, not a traditional username and password form submission for every request.
Testing the Full Authentication Flow
Everything is now in place. Let's test it. To do that, we need a protected endpoint.
-
Create a Test Controller:
// Create a new controller, e.g., GreetingsController.java package com.example.yourproject.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/greetings") public class GreetingsController { @GetMapping public ResponseEntity<String> sayHello() { return ResponseEntity.ok("Hello from a secured endpoint!"); } } -
Run and Test:
Start your application and use a tool like Postman.-
Test 1: Accessing without a token.
Send aGETrequest tohttp://localhost:8080/api/greetings. You should receive a403 Forbiddenresponse. This is because our filter chain ran, found noAuthorizationheader, and the request proceeded to the authorization step, which requires authentication for this endpoint. -
Test 2: Getting a token.
Send aPOSTrequest tohttp://localhost:8080/auth/loginwith the correct credentials for a registered user. Copy thetokenfrom the response body. -
Test 3: Accessing with a valid token.
Create a newGETrequest tohttp://localhost:8080/api/greetings.- Go to the "Authorization" tab.
- Select "Bearer Token" as the type.
- Paste the token you copied into the "Token" field.
- Send the request. You should now receive a
200 OKresponse with the message "Hello from a secured endpoint!".
-
Test 4: Accessing with an invalid token.
Try sending the same request but modify the token slightly (e.g., delete a character). You should again receive a403 Forbiddenerror, because the token signature will no longer be valid.
-
Conclusion
Excellent work! You have now implemented the complete, stateless JWT authentication flow. We've built a custom filter that acts as a bouncer for our API, checking the credentials (the JWT) of every request to a protected resource.
Key Takeaways:
- Spring Security uses a
FilterChainto apply security rules to incoming requests. - We can add custom logic by creating a filter, typically by extending
OncePerRequestFilter. - The JWT validation filter is responsible for extracting the token, validating its signature and expiration, and loading user details.
- The
SecurityContextHolderis updated with anAuthenticationobject to make the user "logged in" for the duration of the request. - The custom filter must be registered in the
SecurityFilterChainusingaddFilterBeforeto place it correctly.
Our API is now secure and functional. However, for other developers (or even ourselves in the future) to use it effectively, it needs clear documentation. In our next lesson, we will address this by integrating Swagger (OpenAPI) to automatically generate interactive API documentation.