Custom Evaluators
Custom-Evaluatoren erweitern das Sicherheitssystem von webforJ um spezialisierte Zugriffssteuerungslogik, die über die grundlegende Authentifizierung und Rollenprüfungen hinausgeht. Verwenden Sie sie, wenn Sie dynamische Bedingungen überprüfen müssen, die vom Kontext der Anfrage abhängen, nicht nur von den Berechtigungen des Benutzers.
Dieser Leitfaden behandelt benutzerdefinierte Evaluatoren für Spring Security. Wenn Sie Spring Boot nicht verwenden, siehe den Evaluator-Chain-Leitfaden, um zu verstehen, wie Evaluatoren funktionieren, und Vollständige Implementierung für ein funktionierendes Beispiel.
Was sind benutzerdefinierte Evaluatoren
Ein Evaluator bestimmt, ob ein Benutzer auf eine spezifische Route basierend auf benutzerdefinierter Logik zugreifen kann. Evaluatoren werden während der Navigation überprüft, bevor eine Komponente gerendert wird, was es Ihnen ermöglicht, den Zugriff dynamisch abzufangen und zu steuern.
webforJ enthält integrierte Evaluatoren für standardmäßige Jakarta-Anmerkungen:
AnonymousAccessEvaluator- Behandelt@AnonymousAccessPermitAllEvaluator- Behandelt@PermitAllRolesAllowedEvaluator- Behandelt@RolesAllowedDenyAllEvaluator- Behandelt@DenyAll
Benutzerdefinierte Evaluatoren folgen demselben Muster, sodass Sie Ihre eigenen Anmerkungen und Zugriffssteuerungslogik erstellen können.
Für Details zu @AnonymousAccess, @PermitAll, @RolesAllowed und @DenyAll, siehe den Leitfaden zu Sicherheitsanmerkungen.
Anwendungsfall: Besitzverifizierung
Ein häufiges Erfordernis ist es, Benutzern den Zugriff nur auf ihre eigenen Ressourcen zu erlauben. Beispielsweise sollten Benutzer nur ihr eigenes Profil bearbeiten können, nicht das Profil eines anderen.
Das Problem: @RolesAllowed("USER") gewährt den Zugriff auf alle authentifizierten Benutzer, überprüft jedoch nicht, ob der Benutzer auf seine eigene Ressource zugreift. Sie müssen die Benutzer-ID des angemeldeten Benutzers mit der Ressourcen-ID in der URL vergleichen.
Beispielszenario:
- Benutzer-ID
123ist angemeldet - Sie navigieren zu
/users/456/edit - Dürfen sie auf diese Seite zugreifen? NEIN - sie können nur
/users/123/editbearbeiten.
Sie können dies nicht mit Rollen lösen, da es von dem Routenparameter :userId abhängt, der sich für jede Anfrage ändert.
Erstellen einer benutzerdefinierten Anmerkung
Definieren Sie eine Anmerkung, um Routen zu kennzeichnen, die eine Besitzverifizierung erfordern:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireOwnership {
/**
* Der Name des Routenparameters, der die Benutzer-ID enthält.
*/
String value() default "userId";
}
Verwenden Sie sie auf Routen, die Besitzprüfungen erfordern:
@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("Profil bearbeiten");
}
}
Implementierung des Evaluators
Erstellen Sie einen von Spring verwalteten Evaluator, der die angemeldete Benutzer-ID mit dem Routenparameter vergleicht:
@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) {
// Zuerst die Authentifizierung überprüfen
if (!securityContext.isAuthenticated()) {
return RouteAccessDecision.denyAuthentication();
}
// Annotation abrufen
RequireOwnership annotation = routeClass.getAnnotation(RequireOwnership.class);
String paramName = annotation.value();
// Die angemeldete Benutzer-ID aus dem Sicherheitskontext abrufen
String currentUserId = securityContext.getPrincipal()
.filter(p -> p instanceof UserDetails)
.map(p -> ((UserDetails) p).getUsername())
.orElse(null);
// :userId von den Routenparametern abrufen
String requestedUserId = context.getRouteParameters()
.get(paramName)
.orElse(null);
// Überprüfen, ob sie übereinstimmen
if (currentUserId != null && currentUserId.equals(requestedUserId)) {
// Besitz verifiziert - Kette fortsetzen, um anderen Evaluatoren zu erlauben, zu laufen
return chain.evaluate(routeClass, context, securityContext);
}
return RouteAccessDecision.deny("Sie können nur auf Ihre eigenen Ressourcen zugreifen");
}
}
Spring entdeckt und registriert automatisch Evaluatoren, die mit @RegisteredEvaluator annotiert sind.
Wie es funktioniert
Die Implementierung des Evaluators hat zwei Schlüsselmethoden:
supports(Class<?> routeClass)
- Gibt
truezurück, wenn dieser Evaluator die Route handhaben soll - Nur Evaluatoren, die
truezurückgeben, werden für die Route aufgerufen - Filtert Routen, indem es nach der
@RequireOwnership-Anmerkung sucht
evaluate(...)
- Überprüft zuerst, ob der Benutzer authentifiziert ist
- Holt die angemeldete Benutzer-ID aus
securityContext.getPrincipal() - Holt den Wert des Routenparameters aus
context.getRouteParameters().get(paramName) - Vergleicht die beiden IDs
- Wenn sie übereinstimmen, delegiert sie an
chain.evaluate(), um anderen Evaluatoren zu erlauben, zu laufen - Wenn sie nicht übereinstimmen, gibt sie
deny()mit einem Grund zurück
Flussbeispiel
Wenn die Besitzprüfung fehlschlägt:
- Benutzer
123meldet sich an und navigiert zu/users/456/edit OwnershipEvaluator.supports()gibttruezurück (Route hat@RequireOwnership)OwnershipEvaluator.evaluate()läuft:currentUserId = "123"(aus dem Sicherheitskontext)requestedUserId = "456"(vom Routenparameter:userId)"123".equals("456")→false- Gibt
RouteAccessDecision.deny("Sie können nur auf Ihre eigenen Ressourcen zugreifen")zurück
- Benutzer wird zur Zugriffsverweigerungsseite umgeleitet
Wenn die Besitzprüfung besteht:
- Benutzer
123meldet sich an und navigiert zu/users/123/edit OwnershipEvaluator.evaluate()läuft:currentUserId = "123",requestedUserId = "123"- IDs stimmen überein → ruft
chain.evaluate()auf, um fortzufahren
- Wenn keine anderen Evaluatoren den Zugriff verweigern, wird dem Benutzer der Zugriff gewährt
Verständnis der Evaluator-Kette
Das Sicherheitssystem verwendet ein Chain of Responsibility-Muster, bei dem Evaluatoren in prioritärer Reihenfolge verarbeitet werden. Evaluatoren können entweder terminale Entscheidungen treffen oder an die Kette delegieren, um mehrere Prüfungen zu kombinieren.
Wie die Kette funktioniert
- Evaluatoren werden nach Priorität sortiert (niedrigere Zahlen zuerst)
- Für jeden Evaluator wird
supports(routeClass)aufgerufen, um zu überprüfen, ob er zutrifft - Wenn
supports()truezurückgibt, wird dieevaluate()-Methode des Evaluators aufgerufen - Der Evaluator kann entweder:
- Eine terminale Entscheidung zurückgeben (
grant()oderdeny()) - stoppt die Kette - An die Kette delegieren, indem er
chain.evaluate()aufruft - ermöglicht es anderen Evaluatoren, zu laufen
- Eine terminale Entscheidung zurückgeben (
- Wenn die Kette ohne Entscheidung abgeschlossen wird und secure-by-default aktiviert ist, werden nicht authentifizierte Benutzer abgelehnt
Terminale Entscheidungen
Stoppt die Kette sofort:
RouteAccessDecision.grant()
- Gewährt Zugang und stoppt die weitere Evaluierung
- Wird von
@AnonymousAccessund@PermitAllverwendet - dies sind vollständige Genehmigungen, die nicht mit anderen Prüfungen kombiniert werden
RouteAccessDecision.deny(reason)
- Verweigert den Zugang und stoppt die weitere Evaluierung
- Wird von
@DenyAllund wenn benutzerdefinierte Prüfungen fehlschlagen, verwendet - Beispiel:
RouteAccessDecision.deny("Sie können nur auf Ihre eigenen Ressourcen zugreifen")
RouteAccessDecision.denyAuthentication()
- Leitet zur Anmeldeseite weiter
- Wird verwendet, wenn Authentifizierung erforderlich, aber nicht vorhanden ist
Kettendelegation
Ermöglicht das Kombinieren von Prüfungen:
chain.evaluate(routeClass, context, securityContext)
- Überträgt die Kontrolle an den nächsten Evaluator in der Kette
- Ermöglicht das Kombinieren mehrerer Autorisierungsprüfungen
- Wird von
@RolesAllowedund@RouteAccessverwendet, nachdem ihre Prüfungen bestanden haben - Benutzerdefinierte Evaluatoren sollten dieses Muster verwenden, wenn Prüfungen bestehen, um eine Zusammensetzung zu ermöglichen
Evaluator-Priorität
Evaluatoren werden in prioritärer Reihenfolge überprüft (niedrigere Zahlen zuerst). Framework-Evaluator verwenden die Priorität 1-9, benutzerdefinierte Evaluatoren sollten 10 oder höher verwenden.
Integrierte Evaluatoren werden in dieser Reihenfolge registriert:
// Priorität 1: @DenyAll - blockiert alles
// Priorität 2: @AnonymousAccess - erlaubt nicht authentifizierten Zugriff
// Priorität 3: AuthenticationRequiredEvaluator - stellt sicher, dass Auth für @PermitAll/@RolesAllowed erforderlich ist
// Priorität 4: @PermitAll - erfordert nur Authentifizierung
// Priorität 5: @RolesAllowed - erfordert spezifische Rollen
// Priorität 6: @RouteAccess - SpEL-Ausdrücke (nur Spring Security)
// Priorität 10+: Benutzerdefinierte Evaluatoren (wie @RequireOwnership)
Wie Priorität die Evaluierung beeinflusst
- Evaluatoren mit niedrigerer Priorität laufen zuerst und können die Kette "kurzschließen"
@DenyAll(Priorität 1) läuft zuerst - wenn vorhanden, wird der Zugriff immer verweigert@AnonymousAccess(Priorität 2) läuft als nächstes - wenn vorhanden, wird der Zugriff immer gewährt (auch ohne Auth)AuthenticationRequiredEvaluator(Priorität 3) überprüft, ob die Route Authentifizierung benötigt und der Benutzer authentifiziert ist- Wenn kein Evaluator die Route behandelt, gelten secure-by-default-Regeln
Priorität festlegen
Setzen Sie die Priorität mit der Annotation @RegisteredEvaluator:
@RegisteredEvaluator(priority = 10) // Läuft nach integrierten Evaluatoren
public class OwnershipEvaluator implements RouteSecurityEvaluator {
// ...
}
Benutzerdefinierte Evaluatoren sollten Priorität 10 oder höher verwenden. Prioritäten 1-9 sind für Framework-Evaluatoren reserviert. Wenn Sie eine Priorität im reservierten Bereich verwenden, erhalten Sie eine Warnung in den Protokollen.
Kombinieren von Evaluatoren
Evaluatoren, die an die Kette delegieren, können kombiniert werden, um komplexe Autorisierungslogik zu erstellen. Routen können mehrere Sicherheitsanmerkungen haben:
Kombinieren von Rollenprüfungen mit benutzerdefinierter Logik
@Route("/users/:userId/settings")
@RolesAllowed("USER")
@RequireOwnership("userId")
public class UserSettingsView extends Composite<Div> {
// Muss die Rolle USER haben UND auf ihre eigenen Einstellungen zugreifen
}
Wie es funktioniert:
RolesAllowedEvaluator(Priorität 5) überprüft, ob der Benutzer die Rolle "USER" hat- Wenn ja, ruft er
chain.evaluate()auf, um fortzufahren OwnershipEvaluator(Priorität 10) überprüft, obuserIdmit dem angemeldeten Benutzer übereinstimmt- Wenn ja, ruft er
chain.evaluate()auf, um fortzufahren - Die Kette endet → Zugriff gewährt
Kombinieren von SpEL-Ausdrücken mit benutzerdefinierter Logik
@Route("/admin/users/:userId/edit")
@RouteAccess("hasRole('ADMIN')")
@RequireOwnership("userId")
public class AdminEditUserView extends Composite<Div> {
// Muss Admin sein UND auf ihr eigenes Konto zugreifen
}
Was kann nicht kombiniert werden
@AnonymousAccess und @PermitAll treffen terminal Entscheidungen - sie gewähren sofort Zugriff, ohne die Kette aufzurufen. Sie können sie nicht mit benutzerdefinierten Evaluatoren kombinieren:
// @PermitAll gewährt sofort Zugang, @RequireOwnership wird niemals ausgeführt
@Route("/users/:userId/profile")
@PermitAll
@RequireOwnership("userId")
public class ProfileView extends Composite<Div> {
// ...
}
Für Ressourcen, auf die alle authentifizierten Benutzer zugreifen können, verwenden Sie @RolesAllowed mit einer allgemeinen Rolle stattdessen:
// @RolesAllowed delegiert an die Kette
@Route("/users/:userId/profile")
@RolesAllowed("USER")
@RequireOwnership("userId")
public class ProfileView extends Composite<Div> {
// Muss ein authentifizierter Benutzer sein UND auf sein eigenes Profil zugreifen
}