Method Security
Method security puts the authorization check right at the business method, not only at the route or the REST endpoint. jSentinel offers two paths to the same enforcement — pick per type, mix freely:
| Path | Module | Works on | When |
|---|---|---|---|
Compile-time processor — @Secured → generated <Type>Secured | security-processor | concrete classes | No runtime reflection, clean stacktraces, works with final methods via a generated subclass |
Runtime dynamic proxy — SecuredProxy.wrap(Interface, impl) | security-standalone | interfaces | No build-time step, decide at runtime |
Both paths call the same SecurityEnforcer, so a permission rule
behaves identically regardless of which path expressed it.
Annotations
The same generic annotations used for Vaadin views and REST handlers apply to methods:
public interface LibraryService {
@RequiresPermission("book:list")
List<String> listBooks();
@RequiresRole("ADMIN")
void removeBook(String title);
@RequiresPolicy("book.owner-or-librarian")
void editBook(String title, String patch);
}New in 00.70: @RequiresAnyPermission({...}) and
@RequiresAllPermissions({...}) for multi-permission shortcuts, plus
@RequiresPolicy("name") for the Policy API.
Path A — Runtime dynamic proxy
LibraryService library =
SecuredProxy.wrap(LibraryService.class, new InMemoryLibraryService());
library.listBooks(); // throws AccessDeniedException if subject lacks book:list
library.removeBook("Frank"); // requires role ADMINSecuredProxy.wrap(...) returns a java.lang.reflect.Proxy. For each
invocation it scans the method for security annotations, resolves the
current subject, runs the SecurityEnforcer, then either delegates or
throws AccessDeniedException. Unannotated methods pass through
unchanged.
For single callbacks or lambdas where an interface isn’t a clean fit:
SecuredProxy.requireAllowed(MyHandler.class, "deleteSelected");
documentService.delete(selectedId);Path B — Compile-time annotation processor
Add security-processor at provided scope (see
Quick Start) and
annotate a concrete class with @Secured:
@Secured
public class MemberDirectory {
@RequiresRole("ADMIN")
public void deactivate(MemberId id) { ... }
}At compile time the processor generates a MemberDirectorySecured
subclass that inserts SecurityEnforcer.require…(…) ahead of each
annotated super.<method>(…):
MemberDirectory dir = new MemberDirectorySecured(realEnforcer, ...);
dir.deactivate(id); // enforced — generated wrapper checks ADMIN firstThe generated wrapper carries
@GeneratedByProxyBuilder(...) (RUNTIME-reflectable) and a
@DelegatesTo("Owner#method(params)") marker per method. Built on
com.svenruppert:proxybuilder:00.11.00.
Compile errors are raised for final classes/methods or
private / static methods carrying a security annotation —
the processor can’t wrap those, so it fails fast rather than silently
skipping the check.
One enforcer, two paths
demo-standalone exercises both side by side: LibraryService via
SecuredProxy.wrap(...), MemberDirectory via the
processor-generated MemberDirectorySecured. The permission semantics
are identical because both funnel through SecurityEnforcer.
Choosing a path
- Interface already exists, want zero build config → runtime proxy.
- Concrete class,
finalmethods, want no reflection / clean stacktraces / no per-call proxy overhead → compile-time processor. - Mixed → use both; they interoperate through the shared enforcer.