Entry point vs access denied handler

Spring Security · lesson 19 of 31 · 3 min read

Return clean JSON for 401 and 403 by plugging in the two handlers ExceptionTranslationFilter calls.

Open this lesson in the learning hub

Key points

  • ExceptionTranslationFilter sits just above AuthorizationFilter and catches the two exceptions it throws.
  • An AuthenticationException always goes to the AuthenticationEntryPoint: start a login, or answer 401.
  • An AccessDeniedException from 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 AccessDeniedHandler instead: 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 @ControllerAdvice never 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.