TL;DR;
I believe it's just a random filter someone on the internet happened to have chosen and shared his/her code somewhere, and it's been adopted that way ever since.
For the curious
I personally went through the Spring Security source code and found out that the filters in Spring application context with their respective @Order
s are as below:
org.springframework.security.web.access.channel.ChannelProcessingFilter -> 100
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter -> 300
org.springframework.security.web.context.SecurityContextPersistenceFilter -> 400
org.springframework.security.web.header.HeaderWriterFilter -> 500
org.springframework.web.filter.CorsFilter -> 600
org.springframework.security.web.csrf.CsrfFilter -> 700
org.springframework.security.web.authentication.logout.LogoutFilter -> 800
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter -> 900
org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter -> 1000
org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter -> 1100
org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter -> 1200
org.springframework.security.cas.web.CasAuthenticationFilter -> 1300
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter -> 1400
org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter -> 1500
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -> 1600
org.springframework.security.openid.OpenIDAuthenticationFilter -> 1800
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter -> 1900
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter -> 2000
org.springframework.security.web.session.ConcurrentSessionFilter -> 2100
org.springframework.security.web.authentication.www.DigestAuthenticationFilter -> 2200
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter -> 2300
org.springframework.security.web.authentication.www.BasicAuthenticationFilter -> 2400
org.springframework.security.web.savedrequest.RequestCacheAwareFilter -> 2500
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter -> 2600
org.springframework.security.web.jaasapi.JaasApiIntegrationFilter -> 2700
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -> 2800
org.springframework.security.web.authentication.AnonymousAuthenticationFilter -> 2900
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter -> 3000
org.springframework.security.web.session.SessionManagementFilter -> 3100
org.springframework.security.web.access.ExceptionTranslationFilter -> 3200
org.springframework.security.web.access.intercept.FilterSecurityInterceptor -> 3300
org.springframework.security.web.authentication.switchuser.SwitchUserFilter -> 3400
Please note that the less the order value, the more prioritized the filter.
But these are all the filters that Spring registers. Spring Security on the other hand, being a single filter chain containing a number of filters inside, includes only some of these, which are (in priority order):
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
LogoutFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
You can get this list by setting the debug flag to true in the Spring Security config by
@EnableWebSecurity(debug = true)
Where to put the custom Jwt Filter ?
I'd personally place the custom Jwt Filter right after ExceptionTranslationFilter
and before FilterSecurityInterceptor
, forming below chain:
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
LogoutFilter
<------ Here's where the UsernamePasswordAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
JwtAuthTokenFilter <------ Here's your filter.
FilterSecurityInterceptor
]
You see the motivation in doing so, is to let Spring security exception handler resides in ExceptionTranslatorFilter
to handle your exception as is.
Because if you place your filter where the UsernamePasswordAuthenticationFilter
would be, any possible exception thrown by your filter will not be seen by the ExceptionTranslatorFilter
, instead it will go all the way back to the throwable()
method of Tomcat's org.apache.catalina.core.StandardHostValve
, (if you're using Tomcat as the servlet impl., obviously), and what it is going to do is, redirect your request to the default error path, i.e. /error
.
Here's where the fun begins; upon an exception thrown by your filter that is supposed to intercept whatever request coming into your application (say /hello
), now a subsequent request to the predefined error path (let's assume /error
) will be made. And by luck, if you haven't happened to "permit" that path with your HttpSecurity
config, what's next is below series of events:
- The same security chain will be applied to the new request with requestURI
/error
- If your custom filter extends
OncePerRequestFilter
, which I assume it would, it will NOT BE FILTERED AGAIN, since it would have been marked as FILTERED in the previous pass.
- The rest of the filter chain will be invoked in the same order above all the way to the last filter, a.k.a
FilterSecurityInterceptor
- It'll evaluate the authentication token, pull in the
AccessDecisionVoter
s, which will promptly vote the unfamiliar /error
request as unauthenticated, throw an AccessDeniedException
and call it a day.
- The very next filter on the way back to the client, which is no other than
ExceptionTranslationFilter
will see this AccessDeniedException
, will evaluate it as a regular access denial and call the AuthenticationEntryPoint.commence
with the AccessDeniedException
being the 3rd parameter.
- From then I would assume your code will call
sendError
within that method, and your client will receive this as a response.
So what's wrong with that, I blocked the request in my filter and the client got the error response ?
What's wrong is, the message client got, was not generated for your request or your error, it was generated for a request to the /error
path and an error generated by the FilterSecurityInterceptor
.
So what should you do ?
I would humbly suggest that, put your JwtAuthTokenFilter
between the last two filters in the security filter chain, and the error you might throw will be handled by default spring security error resolver and you will save the server a roundtrip between the StandardHostValve
and the FilterSecurityInterceptor
.
http.addFilterAfter(authenticationJwtTokenFilter(), ExceptionTranslationFilter.class);
Or don't and the client will get an error eventually.