What is Spring Security's AccessDeniedHandler and when does it run?
answer
- 403 for authenticated-but-unauthorized
- one method: handle(req,res,ex)
- called by ExceptionTranslationFilter
- default = AccessDeniedHandlerImpl
- 401 vs 403 split
basics
~10 sIt's the component that produces the HTTP 403 (Forbidden) response when a logged-in user tries to access something they're not allowed to. It runs after an AccessDeniedException is thrown.
solid answer
~30 sAccessDeniedHandler is a Spring Security interface with one method, handle(request, response, AccessDeniedException). It is invoked by the ExceptionTranslationFilter when authorization fails for a request whose user is already authenticated — meaning they're logged in but lack the required role/authority. The default implementation, AccessDeniedHandlerImpl, sends a 403 Forbidden. This is distinct from authentication failure (not logged in), which yields 401 via a different component. You customize it to return a friendly error page or a JSON body instead of the default blank 403. Configure it through http.exceptionHandling(e -> e.accessDeniedHandler(...)).
code
java · 17 lines// The interface you implement
public interface AccessDeniedHandler {
void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException;
}
// Registering the default page-based variant
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.exceptionHandling(ex -> ex.accessDeniedPage("/error/403"));
return http.build();
}go deeper
Know it produces 403 for an authenticated user who lacks permission, and that 401 is the separate not-logged-in case.
Be able to name ExceptionTranslationFilter as the caller and configure a custom handler via exceptionHandling().
Explain the anonymous-vs-authenticated routing done by AuthenticationTrustResolver and why anonymous denials become 401, not 403.
Reason about where in the request lifecycle the exception is caught and how that determines whether your handler even runs versus MVC exception handling.
**The problem it solves.** In Spring Security there are two very different failure modes: (1) *authentication* failure — the caller is not logged in / has no valid credentials, which should produce **401 Unauthorized**; and (2) *authorization* failure — the caller **is** authenticated but is not permitted to perform this action, which should produce **403 Forbidden**. `AccessDeniedHandler` owns case (2). **The interface.** `org.springframework.security.web.access.AccessDeniedHandler` has a single method: ```java void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException; ``` Its job is to write the 403 response — status code, and optionally a body, headers, or a forward to an error page. **Who calls it.** The `ExceptionTranslationFilter` sits in the security filter chain. It wraps the rest of the chain in a try/catch. When a downstream authorization check (e.g. the `AuthorizationFilter`) throws an `AccessDeniedException`, the filter decides what to do: if the current authentication represents a **real, authenticated** user, it delegates to the `AccessDeniedHandler` (403). If the user is **anonymous** (or authenticated only via remember-me), it instead delegates to the `AuthenticationEntryPoint` to start authentication (401 / redirect to login). That anonymous-vs-authenticated decision is made by an `AuthenticationTrustResolver`. **The default.** If you configure nothing, `AccessDeniedHandlerImpl` runs. It calls `response.sendError(403)` (a blank server 403 page), or, if you set an error page via `accessDeniedPage(...)`, forwards the request there. **When to customize.** REST APIs almost always replace the default so a 403 returns a structured JSON error envelope instead of an HTML page. You provide a `@Component` implementing `AccessDeniedHandler` and register it. **Configuration (Spring Security 6, component-based DSL):** ```java http.exceptionHandling(ex -> ex.accessDeniedHandler(myAccessDeniedHandler)); ``` or the simpler page form: ```java http.exceptionHandling(ex -> ex.accessDeniedPage("/error/403")); ``` **Key terms.** *AccessDeniedException* — a `RuntimeException` in `org.springframework.security.access` thrown when an authorization decision denies access. *ExceptionTranslationFilter* — the filter that catches security exceptions and routes them to the handler or entry point. *AuthenticationEntryPoint* — the sibling component that handles the 401 case (out of scope here). **Common gotcha.** Candidates conflate 401 and 403. Remember: 401 = *who are you?* (not authenticated → AuthenticationEntryPoint); 403 = *I know who you are, you still can't* (authenticated but unauthorized → AccessDeniedHandler).
- What is the difference between what AccessDeniedHandler and AuthenticationEntryPoint handle?AuthenticationEntryPoint handles the 401 case (request is not authenticated — start authentication), AccessDeniedHandler handles the 403 case (request is authenticated but lacks authority). ExceptionTranslationFilter picks between them based on whether the user is anonymous.
- What HTTP status does the default AccessDeniedHandler return?403 Forbidden — via AccessDeniedHandlerImpl, which calls response.sendError(403) or forwards to a configured error page.
saying these in an interview costs you the question
- Saying AccessDeniedHandler returns 401
- Thinking it fires when the user is not logged in
- Confusing it with AuthenticationEntryPoint
- Believing it handles login/credential failures