Custom Evaluators
Custom evaluators breiden het beveiligingssysteem van webforJ uit met gespecialiseerde toegangscontrolelogica die verder gaat dan basisauthenticatie en rolcontroles. Gebruik ze wanneer je dynamische voorwaarden moet verifiëren die afhankelijk zijn van de context van het verzoek en niet alleen van gebruikersrechten.
Deze gids behandelt aangepaste evaluators voor Spring Security. Als je geen Spring Boot gebruikt, zie dan de Evaluator Chain-gids om te begrijpen hoe evaluators werken en Complete Implementatie voor een werkend voorbeeld.
Wat zijn aangepaste evaluators
Een evaluator bepaalt of een gebruiker toegang kan krijgen tot een specifieke route op basis van aangepaste logica. Evaluators worden tijdens de navigatie gecontroleerd voordat een component wordt weergegeven, zodat je de toegang dynamisch kunt onderscheppen en controleren.
webforJ bevat ingebouwde evaluators voor standaard Jakarta-annotaties:
AnonymousAccessEvaluator- Behandelt@AnonymousAccessPermitAllEvaluator- Behandelt@PermitAllRolesAllowedEvaluator- Behandelt@RolesAllowedDenyAllEvaluator- Behandelt@DenyAll
Aangepaste evaluators volgen hetzelfde patroon, zodat je je eigen annotaties en toegangscontrolelogica kunt maken.
Voor details over @AnonymousAccess, @PermitAll, @RolesAllowed en @DenyAll, zie de Beveiligingsannotaties-gids.
Gebruikscase: Eigenaarschap verificatie
Een veelvoorkomend vereiste is om gebruikers alleen toegang te geven tot hun eigen bronnen. Gebruikers mogen bijvoorbeeld alleen hun eigen profiel bewerken, niet dat van iemand anders.
Het probleem: @RolesAllowed("USER") verleent toegang aan alle geverifieerde gebruikers, maar controleert niet of de gebruiker toegang heeft tot zijn eigen bron. Je moet de ID van de ingelogde gebruiker vergelijken met de bron-ID in de URL.
Voorbeeldscenario:
- Gebruiker ID
123is ingelogd - Ze navigeren naar
/users/456/edit - Moeten ze deze pagina mogen openen? NEE - ze kunnen alleen
/users/123/editbewerken
Je kunt dit niet oplossen met rollen omdat het afhankelijk is van de routeparameter :userId, die voor elke aanvraag verandert.
Een aangepaste annotatie maken
Definieer een annotatie om routes te markeren die eigenaarschap verificatie vereisen:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireOwnership {
/**
* De naam van de routeparameter die de gebruikers-ID bevat.
*/
String value() default "userId";
}
Gebruik het op routes die eigenaarschapscontroles vereisen:
@Route(value = "/users/:userId/edit", outlet = MainLayout.class)
@RequireOwnership("userId")
public class EditProfileView extends Composite<Div> {
private final Div self = getBoundComponent();
public EditProfileView() {
self.setText("Bewerk Profiel Pagina");
}
}
De evaluator implementeren
Maak een Spring-beheerde evaluator die de ingelogde gebruikers-ID vergelijkt met de routeparameter:
@RegisteredEvaluator(priority = 10)
public class OwnershipEvaluator implements RouteSecurityEvaluator {
@Override
public boolean supports(Class<?> routeClass) {
return routeClass.isAnnotationPresent(RequireOwnership.class);
}
@Override
public RouteAccessDecision evaluate(Class<?> routeClass, NavigationContext context,
RouteSecurityContext securityContext, SecurityEvaluatorChain chain) {
// Controleer eerst de authenticatie
if (!securityContext.isAuthenticated()) {
return RouteAccessDecision.denyAuthentication();
}
// Haal de annotatie op
RequireOwnership annotation = routeClass.getAnnotation(RequireOwnership.class);
String paramName = annotation.value();
// Haal de ingelogde gebruikers-ID op uit de beveiligingscontext
String currentUserId = securityContext.getPrincipal()
.filter(p -> p instanceof UserDetails)
.map(p -> ((UserDetails) p).getUsername())
.orElse(null);
// Haal :userId op uit de routeparameters
String requestedUserId = context.getRouteParameters()
.get(paramName)
.orElse(null);
// Controleer of ze overeenkomen
if (currentUserId != null && currentUserId.equals(requestedUserId)) {
// Eigenaarschap geverifieerd - ga door met de keten om andere evaluators toe te staan
return chain.evaluate(routeClass, context, securityContext);
}
return RouteAccessDecision.deny("Je kunt alleen toegang krijgen tot je eigen bronnen");
}
}
Spring ontdekt en registreert automatisch evaluators die zijn geannoteerd met @RegisteredEvaluator.
Hoe het werkt
De implementatie van de evaluator heeft twee belangrijke methoden:
supports(Class<?> routeClass)
- Retourneert
trueals deze evaluator de route moet behandelen - Alleen evaluators die
trueretourneren, worden voor de route aangeroepen - Filtert routes door te controleren op de annotatie
@RequireOwnership
evaluate(...)
- Controleert eerst of de gebruiker geverifieerd is
- Haalt de ingelogde gebruikers-ID op uit
securityContext.getPrincipal() - Haalt de waarde van de routeparameter op uit
context.getRouteParameters().get(paramName) - Vergelijkt de twee IDs
- Als ze overeenkomen, geeft hij de controle door aan
chain.evaluate()om andere evaluators uit te laten voeren - Als ze niet overeenkomen, retourneert hij
deny()met een reden
Voorbeeld flow
Wanneer de eigenaarschapscontrole faalt:
- Gebruiker
123logt in en navigeert naar/users/456/edit OwnershipEvaluator.supports()retourneerttrue(route heeft@RequireOwnership)OwnershipEvaluator.evaluate()wordt uitgevoerd:currentUserId = "123"(uit de beveiligingscontext)requestedUserId = "456"(uit de routeparameter:userId)"123".equals("456")→false- Retourneert
RouteAccessDecision.deny("Je kunt alleen toegang krijgen tot je eigen bronnen")
- Gebruiker wordt doorgestuurd naar de pagina met toegang geweigerd
Wanneer de eigenaarschapscontrole slaagt:
- Gebruiker
123logt in en navigeert naar/users/123/edit OwnershipEvaluator.evaluate()wordt uitgevoerd:currentUserId = "123",requestedUserId = "123"- IDs komen overeen → roept
chain.evaluate()aan om door te gaan
- Als geen andere evaluators de toegang weigeren, krijgt de gebruiker toegang
De evaluatorketen begrijpen
Het beveiligingssysteem maakt gebruik van een keten van verantwoordelijkheidspatroon waarbij evaluators in volgorde van prioriteit worden verwerkt. Evaluators kunnen terminale beslissingen nemen of de keten delegeren voor het combineren van meerdere controles.
Hoe de keten werkt
- Evaluators worden gesorteerd op prioriteit (lagere nummers eerst)
- Voor elke evaluator wordt
supports(routeClass)aangeroepen om te controleren of deze van toepassing is - Als
supports()trueretourneert, wordt de methodeevaluate()van de evaluator aangeroepen - De evaluator kan ofwel:
- Een terminale beslissing retourneren (
grant()ofdeny()) - stop de keten - Delegeren aan de keten door
chain.evaluate()aan te roepen - maakt het mogelijk dat andere evaluators draaien
- Een terminale beslissing retourneren (
- Als de keten voltooid is zonder een beslissing en secure-by-default is ingeschakeld, worden niet-geverifieerde gebruikers geweigerd
Terminale beslissingen
Stop de keten onmiddellijk:
RouteAccessDecision.grant()
- Verleent toegang en stopt verdere evaluatie
- Gebruikt door
@AnonymousAccessen@PermitAll- dit zijn complete autorisaties die niet combineren met andere controles
RouteAccessDecision.deny(reason)
- Weigert toegang en stopt verdere evaluatie
- Gebruikt door
@DenyAllen wanneer aangepaste controles falen - Voorbeeld:
RouteAccessDecision.deny("Je kunt alleen toegang krijgen tot je eigen bronnen")
RouteAccessDecision.denyAuthentication()
- Stuur door naar de aanmeldpagina
- Gebruikt wanneer authenticatie vereist is maar ontbreekt
Ketende delegatie
Maakt het combineren van controles mogelijk:
chain.evaluate(routeClass, context, securityContext)
- Geeft de controle door aan de volgende evaluator in de keten
- Maakt het mogelijk om meerdere autorisatiecontroles te combineren
- Gebruikt door
@RolesAlloweden@RouteAccessnadat hun controles zijn doorgegeven - Aangepaste evaluators moeten dit patroon gebruiken wanneer controles slagen om samenstelling mogelijk te maken
Evaluator prioriteit
Evaluators worden in volgorde van prioriteit gecontroleerd (lagere nummers eerst). Framework evaluators gebruiken prioriteit 1-9, aangepaste evaluators moeten 10 of hoger gebruiken.
Ingebouwde evaluators worden in deze volgorde geregistreerd:
// Prioriteit 1: @DenyAll - blokkeert alles
// Prioriteit 2: @AnonymousAccess - staat niet-geverifieerde toegang toe
// Prioriteit 3: AuthenticationRequiredEvaluator - zorgt voor auth voor @PermitAll/@RolesAllowed
// Prioriteit 4: @PermitAll - vereist alleen authenticatie
// Prioriteit 5: @RolesAllowed - vereist specifieke rollen
// Prioriteit 6: @RouteAccess - SpEL-expressies (alleen Spring Security)
// Prioriteit 10+: Aangepaste evaluators (zoals @RequireOwnership)
Hoe prioriteit evaluatie beïnvloedt
- Evaluators met een lagere prioriteit draaien eerst en kunnen de keten "opschorten"
@DenyAll(prioriteit 1) draait als eerste - als deze aanwezig is, wordt toegang altijd geweigerd@AnonymousAccess(prioriteit 2) draait daarna - als deze aanwezig is, wordt toegang altijd verleend (zelfs zonder auth)AuthenticationRequiredEvaluator(prioriteit 3) controleert of de route auth nodig heeft en of de gebruiker geverifieerd is- Als geen enkele evaluator de route behandelt, wordt de secure-by-default-logica toegepast
Prioriteit instellen
Stel prioriteit in met de annotatie @RegisteredEvaluator:
@RegisteredEvaluator(priority = 10) // Draait na ingebouwde evaluators
public class OwnershipEvaluator implements RouteSecurityEvaluator {
// ...
}
Aangepaste evaluators moeten prioriteit 10 of hoger gebruiken. Prioriteiten 1-9 zijn gereserveerd voor framework evaluators. Als je een prioriteit in het gereserveerde bereik gebruikt, ontvang je een waarschuwing in de logs.
Evaluators combineren
Evaluators die delegeren aan de keten kunnen worden gecombineerd om complexe autorisatielogica te creëren. Routes kunnen meerdere beveiligingsannotaties hebben:
Rollencontroles combineren met aangepaste logica
@Route("/users/:userId/settings")
@RolesAllowed("USER")
@RequireOwnership("userId")
public class UserSettingsView extends Composite<Div> {
// Moet de rol USER hebben EN toegang hebben tot hun eigen instellingen
}
Hoe het werkt:
RolesAllowedEvaluator(prioriteit 5) controleert of de gebruiker de rol "USER" heeft- Als dat zo is, roept hij
chain.evaluate()aan om door te gaan OwnershipEvaluator(prioriteit 10) controleert ofuserIdovereenkomt met de ingelogde gebruiker- Als dat zo is, roept hij
chain.evaluate()aan om door te gaan - Ketting eindigt → toegang verleend
SpEL-expressies combineren met aangepaste logica
@Route("/admin/users/:userId/edit")
@RouteAccess("hasRole('ADMIN')")
@RequireOwnership("userId")
public class AdminEditUserView extends Composite<Div> {
// Moet admin zijn EN toegang hebben tot hun eigen account
}
Wat kan niet worden gecombineerd
@AnonymousAccess en @PermitAll nemen terminal beslissingen - ze verlenen onmiddellijk toegang zonder de keten aan te roepen. Je kunt ze niet combineren met aangepaste evaluators:
// @PermitAll verleent onmiddellijk toegang, @RequireOwnership draait nooit
@Route("/users/:userId/profile")
@PermitAll
@RequireOwnership("userId")
public class ProfileView extends Composite<Div> {
// ...
}
Voor bronnen waartoe alle geverifieerde gebruikers toegang hebben, gebruik @RolesAllowed met een algemene rol in plaats daarvan:
// @RolesAllowed delegeert naar de keten
@Route("/users/:userId/profile")
@RolesAllowed("USER")
@RequireOwnership("userId")
public class ProfileView extends Composite<Div> {
// Moet een geverifieerde gebruiker zijn EN toegang hebben tot hun eigen profiel
}