skip to content

AccessDeniedHandler

AccessDeniedHandler decides what an authenticated but unauthorized caller sees, normally a 403 with your error format. Interviewers pair it with the entry point to check you can distinguish 'who are you' from 'you may not'.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is Spring Security's AccessDeniedHandler and when does it run?

level: juniorimportance: must knowfreq 45%

answer

  1. 403 for authenticated-but-unauthorized
  2. one method: handle(req,res,ex)
  3. called by ExceptionTranslationFilter
  4. default = AccessDeniedHandlerImpl
  5. 401 vs 403 split

basics

~10 s

It'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 s

AccessDeniedHandler 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
java
// 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

for a junior

Know it produces 403 for an authenticated user who lacks permission, and that 401 is the separate not-logged-in case.

for a middle

Be able to name ExceptionTranslationFilter as the caller and configure a custom handler via exceptionHandling().

for a senior

Explain the anonymous-vs-authenticated routing done by AuthenticationTrustResolver and why anonymous denials become 401, not 403.

for a principal

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

context

open as a page

How do you make a Spring Security REST API return a custom JSON body on a 403 instead of the default blank page?

level: middleimportance: must knowfreq 40%

basics

~10 s

Implement AccessDeniedHandler, set the response status to 403, set content type to application/json, and write your JSON body. Register it via http.exceptionHandling(e -> e.accessDeniedHandler(yourHandler)).

open as a page

What does the default AccessDeniedHandlerImpl do, and how does accessDeniedPage() differ from providing your own AccessDeniedHandler?

level: middleimportance: should knowfreq 25%

basics

~20 s

The default AccessDeniedHandlerImpl sends a 403, either as a blank server error or by forwarding to a configured error page. accessDeniedPage() is a shortcut that just sets that forward path; a custom AccessDeniedHandler lets you fully control the status, body, and headers.

open as a page

What is AuthorizationDeniedException in Spring Security 6, and how does it relate to AccessDeniedException and the AccessDeniedHandler?

level: seniorimportance: should knowfreq 30%

basics

~10 s

AuthorizationDeniedException is the exception the modern AuthorizationManager-based checks (like @PreAuthorize and AuthorizationFilter) throw when access is denied. It extends AccessDeniedException, so it flows through the same ExceptionTranslationFilter and AccessDeniedHandler to a 403.

open as a page

Why does an anonymous user hitting a protected URL get 401 (redirect to login) rather than a 403 from the AccessDeniedHandler, even though authorization technically failed?

level: principalimportance: should knowfreq 22%

basics

~20 s

Because the ExceptionTranslationFilter checks whether the user is anonymous. If they are, it triggers authentication (401/login) via the AuthenticationEntryPoint instead of the AccessDeniedHandler, since the right fix is to log in, not to show a 403.

open as a page