The labs on this site need JavaScript to run. The same content is written out below.
Learn › Spring › Other features
Security filter chain
Where is it decided whether the response is 401 or 403?
Spring Security is a line of filters placed in front of the controller. A filter near the front checks "who this is" and records it, and a filter near the back decides "whether this user may enter this path". The response is 401 when the user is not identified, and 403 when the user is identified but lacks the required authority. This lab shows where a request is stopped, and with which response it ends, depending on its token and path.
Checked against real execution · All 12 requests were sent to a server running Spring Boot 4.1.1 and Spring Security 7.1.1 and checked against the results
Concepts in this lab
- Authentication and authorization
- Authentication checks who a user is. Authorization decides whether that verified user has permission to use a specific resource or feature. Authentication happens first, and authorization is decided based on its result. In HTTP status codes, a failed authentication corresponds to 401 and a failed authorization corresponds to 403.
- Spring Security and SecurityFilterChain
- Spring Security is Spring's security framework for handling authentication and authorization, and it works as a set of Servlet filters. A SecurityFilterChain defines which security filters apply to which requests and in what order. Before a request reaches the controller, the filters run one after another and do work such as checking the authentication information and checking permissions.
- SecurityContext
- The place where Spring Security keeps the authentication information of the user who sent the current request (who the user is and what permissions the user has). When authentication succeeds, the information is stored here. Later, controllers and services read it from here to look up the current user. By default, it is kept per thread.
- Servlet filter
- This component intercepts and processes requests and responses at the servlet container level, and it is part of the Servlet standard. It acts before a request reaches DispatcherServlet and on the way back of the response, and it is used for work that spans all requests, such as setting the encoding, handling CORS, and authentication. Spring Security also works as a group of these components.
- HTTP status codes
- A three-digit number that shows the result of the server's handling of a request. 2xx means success, 3xx means a move to another location (redirection), 4xx means a problem with the client's request, and 5xx means an error on the server side. Common ones are 200 (success), 201 (created), 400 (bad request), 401 (authentication required), 403 (no permission), 404 (not found), and 500 (server error).
Try these
- Default settings — A path that requires login is requested without a token. 401.
- With a valid token — The request reaches the controller.
- USER requests an admin path — It is known who the user is, but the user lacks the permission. 403.
- ADMIN requests an admin path — The request passes.
- Admin path without a token — The response is 401, not 403. See why.
- Path permitted for everyone — The request passes even without a token.
Running with the default settings
- GET /orders comes in and enters the security filter chain. This is the Servlet filter stage, which comes before the controller.
- JwtAuthenticationFilter: There is no token in the Authorization header. The filter does nothing and passes the request on to the next filter. It does not reject the request right here because the path may be one that does not require authentication.
- AnonymousAuthenticationFilter: If there is still no authentication information at this point, the filter marks the request as coming from an "anonymous user".
- AuthorizationFilter: The rule for this path is authenticated (authenticated users only). The current user is anonymous, so the condition is not met. The filter throws an access denied exception.
- ExceptionTranslationFilter: The filter catches the exception and checks the user. The user is anonymous, so the filter treats this not as "no permission" but as "say who you are first", and it calls the AuthenticationEntryPoint to respond with 401.
- The response goes out, and the SecurityContext that was used for this request is cleared. The information is bound to the thread, so it must be removed before the thread is reused for the next request.
Result of this run
- 401 means "it has not been confirmed who this is" (authentication failed). A request for an admin path without a token gets 401, not 403, because it is set up to answer 401 when a request from an unknown user is denied (without this setup, 403 is sent).
- This response is produced in a filter before the request reaches DispatcherServlet, so it cannot be changed with @ControllerAdvice. To change the format of the response, register your own AuthenticationEntryPoint and AccessDeniedHandler.
Code
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> e
.authenticationEntryPoint((req, res, ex) -> res.setStatus(401)) // not authenticated
.accessDeniedHandler((req, res, ex) -> res.setStatus(403))) // no permission
.build();
}
// JwtAuthenticationFilter: verifies the token in the Authorization header and,
// if it is valid, puts the authentication information into the SecurityContext
Assumptions of this model
The model assumes a common JWT-based setup that does not use sessions. The custom JwtAuthenticationFilter is assumed to pass the request on without rejecting it when the token is missing or invalid (some implementations make this filter return 401 right away for an invalid token). For all 12 combinations, HTTP requests were sent to a real server running Spring Boot 4.1.1 and Spring Security 7.1.1, and it was confirmed that the response code, the place that produced the response (AuthenticationEntryPoint, AccessDeniedHandler), and whether the controller ran match the screen. Two things were learned while doing this. First, if the response for an authentication failure is not specified (that is, if there is no exceptionHandling configuration), a request without a token gets 403, not 401. That is why the exceptionHandling configuration was put in the code on the screen. Second, when insufficient authority was left to the default handler of Spring Security, 401, not 403, came back when USER requested the admin path. This is because the default handler passes the request on again to the error page (/error), and that request has no authentication information, so it is handled as an authentication failure once more. To get 403 as on the screen, the handler must respond with the status code directly, or the /error path must be permitted. A real chain has more filters that are not listed here.
Previous lab: Cache abstraction