1
0
Files
spring-security-demo/method-security/docs/02-spel-reference.md
asmhatre 5e9e7f1b12 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
2026-08-25 02:01:29 +00:00

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 →