Entry point vs access denied handler
Return clean JSON for 401 and 403 by plugging in the two handlers ExceptionTranslationFilter calls.
Open this lesson in the learning hubKey points
ExceptionTranslationFiltersits just aboveAuthorizationFilterand catches the two exceptions it throws.- An
AuthenticationExceptionalways goes to theAuthenticationEntryPoint: start a login, or answer 401. - An
AccessDeniedExceptionfrom an anonymous or remember-me caller also goes there, because logging in might fix it. - From a fully logged-in user it goes to the
AccessDeniedHandlerinstead: 403, and no login prompt. - The defaults are browser-shaped: a redirect to /login or a Basic auth popup. An API should override both.
- These handlers write the response themselves, so
@ControllerAdvicenever sees them - the controller never ran.
Example
static void json(HttpServletResponse res, int status, String detail) throws IOException {
res.setStatus(status);
res.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
new ObjectMapper().writeValue(res.getWriter(),
Map.of("status", status, "detail", detail));
}
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> json(res, 401, "login required"))
.accessDeniedHandler((req, res, e) -> json(res, 403, "not allowed")))
.build();
}
Same exception, two exits: anonymous gets 401, a real user gets 403.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Security course, and every lesson in it is listed on the Spring Security contents page.