skip to content

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

level: middleimportance: should knowfreq 25%

answer

  1. AccessDeniedHandlerImpl = default
  2. no page → sendError(403)
  3. page set → forward + WebAttributes.ACCESS_DENIED_403 attr
  4. accessDeniedPage = sugar over setErrorPage
  5. custom handler = full control (APIs)

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.

solid answer

~40 s

AccessDeniedHandlerImpl is Spring Security's default handler. If no error page is set, it calls response.sendError(403), which the servlet container renders as its standard 403 page. If you call http.exceptionHandling(e -> e.accessDeniedPage("/403")), the handler instead forwards (server-side) to that path with the AccessDeniedException exposed as a request attribute, still with a 403 status. That's convenient for server-rendered apps. For REST/JSON, or when you need custom headers or a specific body shape, you implement AccessDeniedHandler yourself and register it via accessDeniedHandler(...) — you then own the entire response. accessDeniedPage and accessDeniedHandler are mutually exclusive ways to configure the same slot; the custom handler is strictly more flexible.

code

java · 12 lines
java
// Option A: server-rendered app — forward to a themed 403 view
http.exceptionHandling(e -> e.accessDeniedPage("/error/403"));

// Option B: REST API — full control over the 403 response
http.exceptionHandling(e -> e.accessDeniedHandler((req, res, ex) -> {
    res.setStatus(HttpServletResponse.SC_FORBIDDEN);
    res.setHeader("Cache-Control", "no-store");
    res.setContentType(MediaType.APPLICATION_JSON_VALUE);
    res.getWriter().write("{\"error\":\"forbidden\"}");
}));

// A and B configure the SAME slot — choose one, not both.

go deeper

for a junior

Know the default returns 403 and that you can point it at an error page.

for a middle

Distinguish accessDeniedPage (forward to a view) from a custom AccessDeniedHandler (full control), and know they share one slot.

for a senior

Explain the forward vs sendError behavior, the request attribute exposed, and Boot's /error interaction.

for a principal

Decide when default/Boot error handling suffices versus an explicit handler, weighing testability and coupling to MVC error infrastructure.

**The default.** When you configure nothing, `org.springframework.security.web.access.AccessDeniedHandlerImpl` is used. Its behavior: - **No error page configured:** `response.sendError(HttpServletResponse.SC_FORBIDDEN, ...)` — the servlet container returns its default 403 page (in Spring Boot, this is routed to the `/error` mapping / `BasicErrorController`, which may render Whitelabel HTML or a JSON error depending on the request's `Accept` header). - **Error page configured** (via `setErrorPage(...)`): it sets status 403, exposes the exception under the request attribute `WebAttributes.ACCESS_DENIED_403` (`RequestDispatcher.forward`), and **forwards** the request to that path. Because it's a forward (not a redirect), the URL in the browser stays the same and no extra round trip occurs. **`accessDeniedPage(String)`.** This DSL method is sugar: internally it constructs an `AccessDeniedHandlerImpl`, calls `setErrorPage(page)`, and installs it. Use it for server-side-rendered apps where a friendly 403 view suffices. It does **not** let you change the status code or write a custom body directly — it forwards to a view that does that. **Custom `AccessDeniedHandler` via `accessDeniedHandler(...)`.** You implement the interface and take full control: set any status (normally 403), any content type, write JSON/text, add headers, log, emit metrics, etc. This is the choice for APIs. **Mutual exclusivity.** Both `accessDeniedPage` and `accessDeniedHandler` configure the same single slot on `ExceptionHandlingConfigurer`. Setting both is contradictory — the last one wins / configuration is ambiguous; pick one. In practice, if you need anything beyond a plain forward-to-view, use `accessDeniedHandler`. **Boot interaction gotcha.** With the default handler and `sendError`, Spring Boot's error handling takes over via `/error`. If you've customized `ErrorController` or error attributes, your 403 body comes from there — meaning you might get consistent errors *without* a custom `AccessDeniedHandler` at all, as long as the exception reaches `sendError`. But relying on that couples your security errors to MVC error handling and only works when the exception reaches the container; a dedicated `AccessDeniedHandler` is more explicit and testable. **When to use which.** - Server-rendered app, want a themed 403 page → `accessDeniedPage("/403")`. - REST/JSON API, custom envelope, headers, logging → custom `AccessDeniedHandler`. - Minimal app, default container/Boot 403 acceptable → configure nothing.

  • Does accessDeniedPage() redirect or forward to the error page?
    It forwards (server-side RequestDispatcher.forward) with a 403 status and the AccessDeniedException stored under the WebAttributes.ACCESS_DENIED_403 request attribute — no client redirect, so the browser URL is unchanged.

saying these in an interview costs you the question

  • Saying accessDeniedPage issues a client redirect (302)
  • Thinking you can set both accessDeniedPage and accessDeniedHandler simultaneously
  • Believing the default handler returns 401
  • Assuming a custom handler is always required even for simple server-rendered 403 pages

context