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
4.5 KiB
← 07 · ordering and transactions · chapter index · next: the audit checklist →
08 · Meta-annotations, templates, class-level rules, ambiguity
Run: java -cp target/classes:$(cat cp.txt) com.ankurm.methodsec.Demo8MetaAnnotations
Output: output/demo8.txt
Source: Demo8MetaAnnotations.java
Naming a rule
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin { }
@IsAdmin now works anywhere @PreAuthorize("hasRole('ADMIN')") would, with no configuration.
The value is not brevity — it is that the rule exists in one place, so changing it is an edit
rather than a grep.
Templated meta-annotations, and a documentation correction
@PreAuthorize("hasRole('{value}')")
public @interface HasRole { String value(); }
The reference documentation says you must publish an AnnotationTemplateExpressionDefaults
bean for {value} to be substituted. In Spring Security 7.1.1 you do not.
output/demo8.txt shows @HasRole("ADMIN") discriminating correctly
between an admin and a non-admin in a context with no such bean.
The reason is in PreAuthorizeExpressionAttributeRegistry, which initialises its scanner as
SecurityAnnotationScanners.requireUnique(PreAuthorize.class) — and that overload constructs
new AnnotationTemplateExpressionDefaults() internally. The bean is autowired
@Autowired(required = false) and only replaces that default. Its single knob is
setIgnoreUnknown(false), which turns an unrecognised placeholder into an error instead of
leaving it in the expression. That is worth publishing the bean for; making templates work at
all is not.
One caveat, read from the source rather than executed here:
SecurityAnnotationScanners.requireUnique memoises scanners in a static map keyed by
annotation type. Within a single JVM the first configuration for a given annotation type is the
one that sticks — relevant if you run several differently-configured contexts in one test JVM.
Multi-value templates need the quotes inside the attribute, which is as awkward as it looks:
@PreAuthorize("hasAnyRole({roles})")
public @interface HasAnyRole { String[] roles(); }
@HasAnyRole(roles = { "'USER'", "'ADMIN'" })
Class level
@PreAuthorize on the class applies to every method. A method-level @PreAuthorize replaces
it rather than adding to it — the nearest declaration wins:
inherited from the class (needs ADMIN) DENIED
method-level overrides it (needs USER) ALLOWED
Different annotation types are ANDed. A class-level @PreAuthorize and a method-level
@PostAuthorize both have to pass.
The trap is the override. Adding @PreAuthorize("hasRole('USER')") to one method of an
@PreAuthorize("hasRole('ADMIN')") class looks like a tightening and is a widening.
Two interfaces, two rules
public interface ReadsAsUser { @PreAuthorize("hasRole('USER')") String read(); }
public interface ReadsAsAdmin { @PreAuthorize("hasRole('ADMIN')") String read(); }
public class Ambiguous implements ReadsAsUser, ReadsAsAdmin { public String read() { ... } }
Spring Security refuses to pick one:
AnnotationConfigurationException: Please ensure there is one unique annotation of type
[interface ...PreAuthorize] attributed to public abstract java.lang.String ...ReadsAsUser.read().
Found 2 competing annotations: [@PreAuthorize("hasRole('USER')"), @PreAuthorize("hasRole('ADMIN')")]
Correction to the common description: this is not a startup failure. The context
refreshes, the bean is proxied, and the exception appears the first time the method is called.
Guidance that says "if a class inherits the same annotation from two interfaces, startup fails"
describes a friendlier framework than the one you are running. Verified in
output/demo8.txt.
The fix is to annotate the implementation method, which is the nearest declaration and wins
outright. The broader lesson is to put authorization rules in one layer and keep them there.
Repeating the same annotation twice on one method is also unsupported — combine with and/or
inside a single expression, or delegate to a bean.
← 07 · ordering and transactions · chapter index · next: the audit checklist →