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
5.5 KiB
← 01 · how it runs · chapter index · next: self-invocation →
02 · The SpEL reference
Run: java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo4SpelReference
Output: output/demo4.txt, output/demo9-with-parameters.txt, output/demo9-without-parameters.txt
Source: Demo4SpelReference.java, 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 versus
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:
@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:
@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: with the hierarchy above, root holding
only ROLE_ADMIN passes hasRole('GUEST').
← 01 · how it runs · chapter index · next: self-invocation →