1
0

Split into per-article modules and add the method-security module

Moves the existing virtual-thread/context-propagation project into
context-propagation/ and adds method-security/ for the Spring Security 7
method-security article: nine runnable demos, fourteen assertions, and every
transcript the article quotes, regenerated by scripts/run-all.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
This commit is contained in:
2026-08-25 02:01:29 +00:00
parent 9f950bffa9
commit 5e9e7f1b12
65 changed files with 4088 additions and 119 deletions

View File

@@ -0,0 +1,117 @@
[← 01 · how it runs](01-how-method-security-runs.md) · [chapter index](README.md) · [next: self-invocation →](03-self-invocation.md)
# 02 · The SpEL reference
Run: `java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo4SpelReference`
Output: [`output/demo4.txt`](output/demo4.txt), [`output/demo9-with-parameters.txt`](output/demo9-with-parameters.txt), [`output/demo9-without-parameters.txt`](output/demo9-without-parameters.txt)
Source: [`Demo4SpelReference.java`](../src/main/java/com/ankurm/methodsec/Demo4SpelReference.java), [`Demo9ParameterNames.java`](../src/main/java/com/ankurm/methodsec/Demo9ParameterNames.java)
Every row below is a real annotated method on a real proxied bean in `Demo4`, invoked as two
different users. Nothing here is transcribed from documentation.
## Predicates
| Expression | Meaning |
|---|---|
| `hasRole('ADMIN')` | authority `ROLE_ADMIN` (prefix configurable) |
| `hasAnyRole('A','B')` | any of them |
| `hasAllRoles('A','B')` | all of them |
| `hasAuthority('report:read')` | the authority string, verbatim, no prefix |
| `hasAnyAuthority(..)` / `hasAllAuthorities(..)` | as above |
| `isAuthenticated()` | authenticated and not anonymous |
| `isFullyAuthenticated()` | authenticated, not anonymous, not remember-me |
| `isRememberMe()` / `isAnonymous()` | the two it excludes |
| `permitAll` / `denyAll` | constants; also callable as `permitAll()` / `denyAll()` |
| `hasPermission(target, permission)` | delegates to a `PermissionEvaluator` bean |
| `hasPermission(id, 'type', permission)` | the id/type overload |
`hasAllRoles` and `hasAllAuthorities` are easy to miss — they are on
`SecurityExpressionRoot` alongside the `any` variants.
## The root object
`MethodSecurityExpressionRoot` (package-private, extends
`SecurityExpressionRoot<MethodInvocation>`) exposes exactly:
| Reference | Available in | What it is |
|---|---|---|
| `authentication` | everywhere | the `Authentication` |
| `principal` | everywhere | `authentication.getPrincipal()` |
| `returnObject` | `@PostAuthorize`, `@PostFilter` | the value the method returned |
| `filterObject` | `@PreFilter`, `@PostFilter` | the element currently being tested |
| `#root.this` | everywhere | the target object |
| `read`, `write`, `create`, `delete`, `admin` | everywhere | `String` constants for `hasPermission(..)` |
There is **no positional access to arguments**. `#root.args[0]` fails with
```
EL1008E: Property or field 'args' cannot be found on object of type
'org.springframework.security.access.expression.method.MethodSecurityExpressionRoot'
```
Arguments are bound by name, which brings us to the flag.
## `#parameterName` needs `-parameters`
`@PreAuthorize("#owner == authentication.name")` resolves `#owner` through a
`ParameterNameDiscoverer`. Java only keeps parameter names in the class file when `javac` is
given `-parameters`. Without it the name is `arg0`, `#owner` resolves to nothing, and the
comparison is false.
The same class, same expression, two compilations —
[`demo9-with-parameters.txt`](output/demo9-with-parameters.txt) versus
[`demo9-without-parameters.txt`](output/demo9-without-parameters.txt):
```
compiled with -parameters : true compiled with -parameters : false
byParameterName param[0] : owner byParameterName param[0] : arg0
alice → #owner == name : ALLOWED alice → #owner == name : DENIED
```
It fails closed, which is the good direction, but it fails **silently** — the rule being
enforced is not the rule you wrote. Spring Boot's Maven and Gradle plugins set `-parameters`
for you; a hand-rolled build, a shaded jar, or a module compiled by a different toolchain may
not. `@P("alias")` from `org.springframework.security.core.parameters` does not depend on the
flag, because the name lives in the annotation.
## Beans and types
`@beanName.method(...)` resolves a bean from the context, which is the cleanest way to move a
non-trivial rule out of a string literal and into testable code:
```java
@PreAuthorize("@accountPolicy.canRead(authentication, #id)")
public Account read(long id) { ... }
```
`T(java.time.LocalDate).now().year >= 2020` works too. It is legal, and it is a warning sign:
an expression that needs a type reference is an expression that wants to be a bean method.
## Role prefix and hierarchy, in 7.1
Both moved. `AbstractSecurityExpressionHandler.setRoleHierarchy(..)` is **deprecated** in
Spring Security 7.1 — the compiler says so, which is how this module found out. The current
knob is an `AuthorizationManagerFactory`:
```java
@Bean
static AuthorizationManagerFactory<MethodInvocation> authorizationManagerFactory() {
DefaultAuthorizationManagerFactory<MethodInvocation> factory = new DefaultAuthorizationManagerFactory<>();
factory.setRoleHierarchy(RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("USER")
.role("USER").implies("GUEST")
.build());
factory.setRolePrefix("ROLE_");
return factory;
}
```
`PrePostMethodSecurityConfiguration` autowires both an `AuthorizationManagerFactory` and a
bare `RoleHierarchy` bean (`@Autowired(required = false)` on each), so a plain `RoleHierarchy`
bean still works. The factory is where the prefix and the hierarchy now live together, and it
is the one that is not deprecated.
Verified in [`output/demo4.txt`](output/demo4.txt): with the hierarchy above, `root` holding
only `ROLE_ADMIN` passes `hasRole('GUEST')`.
[← 01 · how it runs](01-how-method-security-runs.md) · [chapter index](README.md) · [next: self-invocation →](03-self-invocation.md)