[← 06 · denial handling](06-denied-handling.md) · [chapter index](README.md) · [next: meta-annotations →](08-meta-annotations.md) # 07 · Ordering, and `@PostAuthorize` vs `@Transactional` Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo6InterceptorOrder` Output: [`output/demo6.txt`](output/demo6.txt) Source: [`Demo6InterceptorOrder.java`](../src/main/java/com/ankurm/methodsec/Demo6InterceptorOrder.java) ## The numbers, read from the enum `AuthorizationInterceptorsOrder`, printed from the running JVM rather than transcribed: | Constant | `getOrder()` | |---|---| | `FIRST` | `-2147483648` | | `PRE_FILTER` | `100` | | `PRE_AUTHORIZE` | `200` | | `SECURED` | `300` | | `JSR250` | `400` | | `SECURE_RESULT` | `450` | | `POST_AUTHORIZE` | `500` | | `POST_FILTER` | `600` | | `LAST` | `2147483647` | And the advisor beans `@EnableMethodSecurity` actually registers: ``` 100 preFilterAuthorizationMethodInterceptor 200 preAuthorizeAuthorizationMethodInterceptor 450 authorizeReturnObjectMethodInterceptor 500 postAuthorizeAuthorizationMethodInterceptor 600 postFilterAuthorizationMethodInterceptor ``` ## Lower order means further out — which flips for "after" advice Lower order = higher precedence = further *out* in the chain. For the two "before" annotations that reads the obvious way: `@PreFilter` (100) runs before `@PreAuthorize` (200). For the two "after" annotations it reads backwards. `@PostAuthorize` (500) sits **further out** than `@PostFilter` (600), so on the way back out `@PostFilter` finishes first and `@PostAuthorize` evaluates `returnObject` against the **already-filtered** value. Two methods, identical except for the expected size, prove it: ```java @PostAuthorize("returnObject.size() == 3") @PostFilter("filterObject != 'c'") public List expectsThree(List in) { return in; } ``` ``` @PostAuthorize returnObject.size() == 3 DENIED @PostAuthorize returnObject.size() == 2 ALLOWED -> [a, b] ``` Three elements went in, the method returned three, and `@PostAuthorize` saw two. If you have both annotations on one method, `returnObject` is not what the method returned. ## The transaction problem Spring's `@Transactional` advisor defaults to `Ordered.LOWEST_PRECEDENCE` (`2147483647`), which is larger than every security order above. So **security wraps transactions**: the transaction commits, and only then does `@PostAuthorize` decide the caller may not see the result. ```java @Transactional @PostAuthorize("returnObject.owner == authentication.name") public Account recordAndReturn(String owner) { this.jdbc.update("insert into audit(owner) values (?)", owner); return new Account(1, owner, 100); } ``` Against a real H2 database, as a caller who is denied: ``` rows before : 0 recordAndReturn("bob") @PostAuthorize DENIED -> AuthorizationDeniedException rows after the denial : 1 ``` The row is there. The caller got a 403 and the write happened anyway. ## Fixing it Move the transaction advisor outside the security advisor, so the `AuthorizationDeniedException` propagates through it and triggers the normal rollback-on-`RuntimeException` rule: ```java @EnableTransactionManagement(order = Integer.MIN_VALUE) ``` Same run, same denial: ``` rows before : 0 recordAndReturn("bob") @PostAuthorize DENIED -> AuthorizationDeniedException rows after the denial : 0 ``` `@EnableMethodSecurity(offset = ...)` shifts every security interceptor by a fixed amount and gets you to the same place from the other side. Use whichever you can reason about later; `@EnableTransactionManagement(order = ...)` names the thing you are actually moving. **But prefer not to need it.** Rolling back on an authorization failure means you have already done the work and are undoing it, and rollback is not universal — a message you published, a file you wrote, an outbound HTTP call are all still gone. The reference documentation's advice holds: do not combine `@PostAuthorize` with a method that writes. Authorize on the way in with `@PreAuthorize` and arguments, or read with `@PostAuthorize` and write from a separate method. ## While you are here: `@Transactional` has the same two traps Everything in [chapter 03](03-self-invocation.md) and [chapter 04](04-non-proxyable-methods.md) applies to `@Transactional` unchanged — same proxy, same overriding rules. A `private @Transactional` method is exactly as inert as a `private @PreAuthorize` one. If you find one, look for the other. [← 06 · denial handling](06-denied-handling.md) · [chapter index](README.md) · [next: meta-annotations →](08-meta-annotations.md)