[← 05 · filtering](05-filtering.md) · [chapter index](README.md) · [next: ordering and transactions →](07-ordering-and-transactions.md) # 06 · Denial: what is thrown, and how to change it Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo7DeniedHandling` Output: [`output/demo7.txt`](output/demo7.txt) Source: [`Demo7DeniedHandling.java`](../src/main/java/com/ankurm/methodsec/Demo7DeniedHandling.java) ## The exception ``` thrown -> org.springframework.security.authorization.AuthorizationDeniedException is AccessDeniedException -> true is AuthorizationDeniedException -> true carries an AuthorizationResult -> ExpressionAuthorizationDecision granted=false ``` Documentation and older posts say "throws `AccessDeniedException`", and code written against that still catches it. But the concrete type is `AuthorizationDeniedException`, and it carries the `AuthorizationResult` that explains the denial — for a SpEL rule, an `ExpressionAuthorizationDecision` that knows which expression failed. That is the difference between an audit log line saying "access denied" and one saying which rule denied it. ## Returning something instead of throwing `@HandleAuthorizationDenied` names a `MethodAuthorizationDeniedHandler` bean, which gets the `MethodInvocation` and the `AuthorizationResult` and returns a value in place of the throw: ```java @PreAuthorize("hasRole('FINANCE')") @HandleAuthorizationDenied(handlerClass = MaskingHandler.class) public String maskedBalance() { return "1,204,993.22"; } ``` ``` maskedBalance() (alice, no ROLE_FINANCE) ALLOWED -> ***masked*** maskedList() (alice, no ROLE_FINANCE) ALLOWED -> [] maskedBalance() (cfo, has ROLE_FINANCE) ALLOWED -> 1,204,993.22 ``` The returned value must be assignable to the method's declared return type, so a handler shared across methods has to inspect it — the demo's handler returns `List.of()` for a `List` return type and a masked string otherwise. A handler that gets this wrong fails with a `ClassCastException` at the call site, which is worse than the denial it was replacing. Use this where a partial answer is genuinely correct — a masked field on a shared DTO, an empty list for a section the user cannot see. Do not use it to make an authorization failure invisible to your own logs. ## `@AuthorizeReturnObject` Moves the check from the method that returns an object onto the object's own accessors: ```java public class Customer { public String getName() { return this.name; } @PreAuthorize("hasAuthority('pii:read')") public String getEmail() { return this.email; } } @AuthorizeReturnObject public Customer findCustomer(String name) { ... } ``` ``` returned instance -> ...Customer$$SpringCGLIB$$0 customer.getName() (no authority needed) ALLOWED -> alice customer.getEmail() (needs 'pii:read') DENIED -> AuthorizationDeniedException ``` The returned object is CGLIB-proxied, which means [chapter 04](04-non-proxyable-methods.md) applies to it in full: the class cannot be `final`, so **a record will not work**, and a `final` getter is not advised. The `Customer` in this demo is deliberately a plain class for exactly that reason. The same thing without the annotation, through the container's `AuthorizationProxyFactory`: ```java AuthorizationProxyFactory factory = ctx.getBean(AuthorizationProxyFactory.class); Customer wrapped = factory.proxy(raw); // generic, no cast needed ``` ``` raw.getEmail() (unproxied object) ALLOWED -> alice@example.com wrapped.getEmail() (proxied object) DENIED -> AuthorizationDeniedException ``` **Package correction:** `AuthorizationProxyFactory` lives in `org.springframework.security.authorization`, not `org.springframework.security.authorization.method` where the reference documentation places it. The implementation, `AuthorizationAdvisorProxyFactory`, is in `…authorization.method`; the interface is one package up. `proxy(T)` is generic and returns `T`. Two more things worth knowing before you reach for it: - The `authorizeReturnObject` advisor is registered at order **450** (`AuthorizationInterceptorsOrder.SECURE_RESULT`) whether or not anything in your application uses `@AuthorizeReturnObject` — visible in [`output/demo6.txt`](output/demo6.txt). - At class level it proxies *every* return value, including `String` and boxed primitives. Publish `AuthorizationAdvisorProxyFactory.TargetVisitor.defaultsSkipValueTypes()` if you go that route. [← 05 · filtering](05-filtering.md) · [chapter index](README.md) · [next: ordering and transactions →](07-ordering-and-transactions.md)