CodeGym /Kursy /Spring Security /OncePerRequestFilter and JWT filter

OncePerRequestFilter and JWT filter

Spring Security
Poziom 24, Lekcja 1
Dostępny

1. The place of JWT authentication in the filter chain

If you look at Spring Security through a beginner’s eyes, it is very tempting to do things “like in a regular application”: read the token in the controller, and if it is there, get the user; if not, throw an exception. It seems simpler. But this is exactly the road that usually leads to code where security is smeared across controllers and services, and then it still does not match what authorizeHttpRequests does.

The key idea of Spring Security in the servlet/MVC world: authentication and authorization are the responsibility of filters, that is, of the infrastructure layer that runs before DispatcherServlet and before your controllers. That is why Spring Security describes the filter chain as the basis of the mechanism: first come the filters that try to authenticate the request, and only then does authorization kick in (roughly: “is this user allowed on this endpoint?”). In the official diagram of Spring Security filter ordering, it is explicitly noted that after the authentication filters, AuthorizationFilter is invoked, and it already makes the access decision.

In our JWT branch, the filter acts as a “translator” between HTTP and the Spring Security model. It takes the raw HTTP request, extracts the bearer token, validates it (we will cover that in the next lecture), and ultimately turns all of this into the standard Authentication object. After that, the rest of Spring Security works as usual: AuthorizationFilter, @PreAuthorize, @AuthenticationPrincipal — they no longer care where the user came from, the important thing is that the user appeared in the context.

If you move this logic into the controller, you get two unpleasant effects. First, the authorizeHttpRequests rules and method security will work “blindly”, because the user has not been restored yet. Second, you will have to repeat checks on every endpoint or write your own “mini-Spring Security” on top of Spring Security. That is a bit like putting a second helmet on top of the first one: inconvenient, not very useful, and it looks odd.

A small diagram to keep in mind:

flowchart TD
  A["HTTP request
Authorization: Bearer ..."] --> B["SecurityFilterChain"] B --> C["JWT filter (ours)
restore Authentication"] C --> D["AuthorizationFilter
check access rules"] D --> E["DispatcherServlet / Controller"]

2. OncePerRequestFilter: the filter scaffold

Filters in the servlet API are an old and harsh thing: like Java before lambdas (it sort of works, but it is hard to smile). So in the Spring world people almost never write a “bare” jakarta.servlet.Filter; instead they take a more convenient wrapper. For our scenario, the most understandable and popular base class is OncePerRequestFilter.

OncePerRequestFilter solves two practical tasks at once. First, it gives you a more convenient doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) method instead of working with ServletRequest/ServletResponse. Second, it helps guarantee “one normal call per request” and reduces the chance of accidental double execution (it does not fix everything in the world, but on average it makes life calmer). In Spring Security documentation, you can see that many built-in filters are also built on this same scaffold: for example, CsrfFilter has doFilterInternal(...) and can override shouldNotFilter(...), and both methods come from OncePerRequestFilter.

Another useful psychological point: when you extend OncePerRequestFilter, you start thinking like Spring Security. This is not “my separate hacky filter”, it is “yet another normal security filter” that lives in the chain alongside the others and does its job.

Important consequence: our custom JWT filter must not contain business logic. This is not the place where we decide whether the USER may edit someone else’s draft. The filter only deals with trying to restore Authentication from the bearer token. All other decisions should stay where they belong — in request-level rules and method security.

3. The JwtAuthFilter scaffold: doFilterInternal and two exits

Before you write even one line about “verifying the token signature”, you need to learn how to write the filter so that it does not break the application. In essence, a filter has two correct completion scenarios: either it passes the request further along (filterChain.doFilter(...)), or it deliberately finishes request processing on its own (for example, returns 401) and does a return. There is no third option: if you “forget” to call the chain, the request will hang somewhere in limbo.

The filter skeleton based on OncePerRequestFilter looks like this:

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.web.filter.OncePerRequestFilter;

public class JwtAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        // Important: if the filter does not send a response itself, it must pass the request further down the chain
        chain.doFilter(req, res); // for now we do nothing, just do not get in the way
    }
}

Yes, this is a “stub”. And that is wonderful. This is the moment when you check the basic mechanics: the filter compiles, it is a Spring bean, it is wired into the chain, and it gets called. Only after that do you add token extraction, validation, and Authentication construction.

Now let us talk about the two exit scenarios in more practical terms. In most cases your filter will do early exits: for example, if the request is not even in the protected API area, the filter should not spend time and attention on it. Or if there is no token, that is fine for a public endpoint, and for a protected endpoint the access rules will handle it later (usually this ends with 401 through the standard infrastructure). That is why inside doFilterInternal you often get a structure like “if it is not my request — pass it along; if it is mine — try to restore the user; if restored — pass it along too”.

And one more practical nuance. Spring Security has a standard AnonymousAuthenticationFilter that supplies anonymous authentication if no other authentication has succeeded. It is useful, but in the JWT branch we want to manage to set the real authentication before the request becomes an “anonymous user” in terms of the SecurityContext. That is why the filter’s position in the chain matters, and that is exactly the topic of the next section.

4. Connecting it in SecurityFilterChain: addFilterBefore and the anchor

When you add a custom filter to Spring Security, the most dangerous thing is thinking that “the main thing is to add it, and the order will sort itself out somehow”. Unfortunately, order does not “sort itself out”. Order is the security behavior. And here Spring helps: HttpSecurity has methods that let you insert your filter before or after already known filters, so that you do not calculate the order by the stars or read the source code at night. In the HttpSecurity API, the methods addFilterBefore and addFilterAfter are explicitly described as a way to register your filter relative to another filter.

In practical terms, it looks like this: you choose an “anchor” filter (some standard filter class), and you tell Spring Security: “put my filter before it” or “after it”.

For orientation, keep a small table in mind (and in your notes):

Method HttpSecurity What it does When it is convenient
addFilterBefore(my, SomeFilter.class) inserts before a known filter when you need to run earlier (our case)
addFilterAfter(my, SomeFilter.class) inserts after a known filter when you want to process another filter’s result
addFilterAt(my, SomeFilter.class) replaces the filter at a position when you are absolutely sure that “I am instead of it” (for a beginner, almost always risky)

Why do we choose addFilterBefore(jwtAuthFilter, AnonymousAuthenticationFilter.class) in the course? Because AnonymousAuthenticationFilter is exactly responsible for the scenario “if nobody authenticated, then let it be anonymous”. In the JWT branch, we want the bearer token to have time to turn the request into an authenticated one before anonymous kicks in. Otherwise you will get strange behavior: the token is there, but an anonymous user has already ended up in the context, and authorization looks at it and says, “Well hello, anonymous. Not letting you in.” In the end you will be looking for a bug “why does the token not work”, while the bug is actually in the ordering.

An example of minimal filter wiring, focusing specifically on the place in the chain rather than the full set of rules:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;

SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
    // Put the JWT filter earlier than Spring sets anonymous authentication
    http.addFilterBefore(jwtAuthFilter, AnonymousAuthenticationFilter.class);
    return http.build();
}

Now let us connect this to our stateless mode. In the stateless model, Spring Security is usually configured so that the application does not create an HttpSession to store authentication, using SessionCreationPolicy.STATELESS. The documentation explicitly describes this as a way to work without sessions.

So when we write a chain for an API, we usually lock in two decisions: “do not create sessions” and “the JWT filter must run early”.

5. Filter boundaries: shouldNotFilter and early return

Even if you wired the filter perfectly into the SecurityFilterChain, there is another small way to hurt yourself: make the filter work everywhere. On GET /api/public/articles, on POST /api/auth/login, on future auth endpoints, on docs, on random non-API URLs... This is not always fatal, but it makes the code more complicated and invites mistakes like “why did login suddenly start requiring a token?”.

OncePerRequestFilter has a tool that solves this problem in a very human way: the shouldNotFilter(HttpServletRequest request) method. It exists as an extension point specifically in OncePerRequestFilter, and even built-in filters override it — for example, in the CsrfFilter API you can see that shouldNotFilter(...) is overridden on top of the base OncePerRequestFilter.

The idea is simple: you return true for requests that you do not want to filter. Then doFilterInternal will not even be called, and you do not have to write an “early return” in every place.

An example for our project (bearer authentication is needed only for protected API, and the public and auth zones are ignored by the filter):

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.filter.OncePerRequestFilter;

public class JwtAuthFilter extends OncePerRequestFilter {
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // Limit the filter's responsibility:
        // bearer authentication is not needed outside the API, and inside the API it is not needed for public and auth zones
        String uri = request.getRequestURI();
        return !uri.startsWith("/api/")
                || uri.startsWith("/api/public/")
                || uri.startsWith("/api/auth/");
    }
}

The second option is an early return inside doFilterInternal. It also works and is sometimes even easier to explain to beginners, because you see all the logic in one method. But it has a downside: doFilterInternal quickly turns into a “sandwich of ifs”, and token validation (which we will do next) starts drowning in infrastructural wrapping.

How do you choose? The rule here is simple: if the filter clearly has a responsibility zone (for example, protected API inside /api/**), then shouldNotFilter makes the code cleaner and smaller. And inside doFilterInternal you leave only the filter’s “real work”: extract token → (later) check → (later) build authentication → pass it along.

Checking the filter in the chain: DEBUG logs

A normal engineering habit: do not take your own word for it that “the filter is wired in”. Check. In that sense Spring Security is quite honest: it can print the list of filters that will be applied, and that gives a great way to diagnose ordering and the very fact that the filter is connected. Spring Security documentation states that the list of filters is printed at the DEBUG level when the application starts, and even gives an example line where filters are listed in DefaultSecurityFilterChain.

To see this locally, it is enough to temporarily enable debug logging for the security package:

logging:
  level:
    # Temporarily enable this to see the composition and order of filters in the chain
    org.springframework.security: DEBUG

After that, when you start the application, you will be able to check two things with your own eyes. First, that your JwtAuthFilter is indeed present in the list. Second, that it is placed before AnonymousAuthenticationFilter (if you use addFilterBefore relative to it). That is the kind of “small thing” that later saves you half a day of debugging and 40 minutes of philosophical talks with the monitor.

If the filter is not in the list, the problem is almost always either that the filter did not become a Spring bean (there is nowhere to inject it), or that you added it to the wrong SecurityFilterChain (for example, the chain does not match the needed requests), or that your chain is not used in the configuration at all (for example, another config got started).

6. Typical mistakes with a JWT filter

At this point, the kinds of mistakes usually appear that look like “well, that is just a small thing”, but in practice they break the whole JWT branch. And the most annoying part is that they break silently, without a nice red error. So it is better to know them in advance — at least so you do not have to explain to the cat why you are angry at SecurityFilterChain again.

Mistake #1: you wrote the filter, but forgot filterChain.doFilter(...).
As a result, the request seems to “hang” or returns strangely, because you intercepted processing and did not hand it off anywhere. In a filter, you always need to consciously choose: either you send a response and do return, or you pass the request further down the chain.

Mistake #2: you wired the filter, but placed it too late.
If you put the JWT filter after the point where the access decision has already been made, then restoring the user has no meaning. Authorization has already run (or already denied access), and your filter is “catching the train that has already left.” That is why we choose addFilterBefore(...) and anchor to something understandable like AnonymousAuthenticationFilter.

Mistake #3: the filter tries to handle /api/auth/login.
The login endpoint exists to issue a token, not to require one. If the filter starts demanding a bearer token on login, you fall into a logical loop: “to get a token, you need a token.” It is like coming to the office for a pass and hearing from security: “Show your pass.” Limit the scope of responsibility with shouldNotFilter(...).

Mistake #4: using javax.servlet.* instead of jakarta.servlet.*.
In Spring Boot 4 and Spring Framework 7, the “old” javax.* is no longer used here. If you accidentally imported the wrong package (the IDE often suggests the old one), you can get strange compilation errors or class conflicts. For the filter and HTTP request in our baseline, we need jakarta.servlet.FilterChain and jakarta.servlet.http.HttpServletRequest/HttpServletResponse.

Mistake #5: the filter is not a Spring bean, but you are trying to inject it.
You write a SecurityFilterChain(HttpSecurity http, JwtAuthFilter filter) configuration, and Spring looks at you like a person calling a cat by a name the cat has not received yet. If the filter is not registered in the context, it cannot be passed into the chain. Use @Component or declare @Bean.

1
Zadanie
Spring Security, poziom 24, lekcja 1
Niedostępne
Znacznikowy filtr JWT oparty na OncePerRequestFilter
Znacznikowy filtr JWT oparty na OncePerRequestFilter
1
Zadanie
Spring Security, poziom 24, lekcja 1
Niedostępne
Wczesny `401` przy nieprawidłowym schemacie `Authorization`
Wczesny `401` przy nieprawidłowym schemacie `Authorization`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION