Custom Evaluators
Les évaluateurs personnalisés étendent le système de sécurité de webforJ avec une logique de contrôle d'accès spécialisée allant au-delà de l'authentification de base et des vérifications de rôle. Utilisez-les lorsque vous devez vérifier des conditions dynamiques qui dépendent du contexte de la requête, et pas seulement des autorisations de l'utilisateur.
Ce guide couvre les évaluateurs personnalisés pour Spring Security. Si vous n'utilisez pas Spring Boot, consultez le guide de la chaîne d'évaluateurs pour comprendre comment fonctionnent les évaluateurs et l'implémentation complète pour un exemple fonctionnel.
Qu'est-ce que des évaluateurs personnalisés
Un évaluateur détermine si un utilisateur peut accéder à une route spécifique sur la base d'une logique personnalisée. Les évaluateurs sont vérifiés lors de la navigation avant que tout composant ne soit rendu, vous permettant d'intercepter et de contrôler l'accès de manière dynamique.
webforJ inclut des évaluateurs intégrés pour des annotations Jakarta standard :
AnonymousAccessEvaluator- Gère@AnonymousAccessPermitAllEvaluator- Gère@PermitAllRolesAllowedEvaluator- Gère@RolesAllowedDenyAllEvaluator- Gère@DenyAll
Les évaluateurs personnalisés suivent le même modèle, vous permettant de créer vos propres annotations et logique de contrôle d'accès.
Pour des détails sur @AnonymousAccess, @PermitAll, @RolesAllowed et @DenyAll, consultez le guide des annotations de sécurité.
Cas d'utilisation : Vérification de la propriété
Une exigence courante consiste à permettre aux utilisateurs d'accéder uniquement à leurs propres ressources. Par exemple, les utilisateurs ne devraient pouvoir modifier que leur propre profil, et non celui de quelqu'un d'autre.
Le problème : @RolesAllowed("USER") accorde l'accès à tous les utilisateurs authentifiés, mais ne vérifie pas si l'utilisateur accède à sa propre ressource. Vous devez comparer l'ID de l'utilisateur connecté avec l'ID de la ressource dans l'URL.
Scénario d'exemple :
- L'ID de l'utilisateur
123est connecté - Il navigue vers
/users/456/edit - Devrait-il accéder à cette page ? NON - il ne peut modifier que
/users/123/edit
Vous ne pouvez pas résoudre cela avec des rôles car cela dépend du paramètre de route :userId, qui change pour chaque requête.
Création d'une annotation personnalisée
Définissez une annotation pour marquer les routes nécessitant une vérification de propriété :
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireOwnership {
/**
* Le nom du paramètre de route qui contient l'ID de l'utilisateur.
*/
String value() default "userId";
}
Utilisez-la sur les routes nécessitant des vérifications de propriété :
@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("Page de modification du profil");
}
}
Implémentation de l'évaluateur
Créez un évaluateur géré par Spring qui compare l'ID de l'utilisateur connecté avec le paramètre de route :
@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) {
// Vérifiez d'abord l'authentification
if (!securityContext.isAuthenticated()) {
return RouteAccessDecision.denyAuthentication();
}
// Obtenez l'annotation
RequireOwnership annotation = routeClass.getAnnotation(RequireOwnership.class);
String paramName = annotation.value();
// Obtenez l'ID de l'utilisateur connecté à partir du contexte de sécurité
String currentUserId = securityContext.getPrincipal()
.filter(p -> p instanceof UserDetails)
.map(p -> ((UserDetails) p).getUsername())
.orElse(null);
// Obtenez :userId à partir des paramètres de route
String requestedUserId = context.getRouteParameters()
.get(paramName)
.orElse(null);
// Vérifiez si les identifiants correspondent
if (currentUserId != null && currentUserId.equals(requestedUserId)) {
// Propriété vérifiée - continuer la chaîne pour permettre à d'autres évaluateurs
return chain.evaluate(routeClass, context, securityContext);
}
return RouteAccessDecision.deny("Vous ne pouvez accéder qu'à vos propres ressources");
}
}
Spring découvre et enregistre automatiquement les évaluateurs annotés avec @RegisteredEvaluator.
Comment ça fonctionne
L'implémentation de l'évaluateur a deux méthodes clés :
supports(Class<?> routeClass)
- Retourne
truesi cet évaluateur doit gérer la route - Seuls les évaluateurs qui retournent
trueseront invoqués pour la route - Filtre les routes en vérifiant l'annotation
@RequireOwnership
evaluate(...)
- Vérifie d'abord si l'utilisateur est authentifié
- Obtient l'ID de l'utilisateur connecté à partir de
securityContext.getPrincipal() - Obtient la valeur du paramètre de route à partir de
context.getRouteParameters().get(paramName) - Compare les deux identifiants
- S'ils correspondent, délègue à
chain.evaluate()pour permettre à d'autres évaluateurs de s'exécuter - S'ils ne correspondent pas, retourne
deny()avec une raison
Exemple de flux
Lorsque la vérification de propriété échoue :
- L'utilisateur
123se connecte et navigue vers/users/456/edit OwnershipEvaluator.supports()retournetrue(la route a@RequireOwnership)OwnershipEvaluator.evaluate()s'exécute :currentUserId = "123"(à partir du contexte de sécurité)requestedUserId = "456"(à partir du paramètre de route:userId)"123".equals("456")→false- Retourne
RouteAccessDecision.deny("Vous ne pouvez accéder qu'à vos propres ressources")
- L'utilisateur est redirigé vers la page d'accès refusé
Lorsque la vérification de propriété réussit :
- L'utilisateur
123se connecte et navigue vers/users/123/edit OwnershipEvaluator.evaluate()s'exécute :currentUserId = "123",requestedUserId = "123"- Les IDs correspondent → appelle
chain.evaluate()pour continuer
- Si aucun autre évaluateur refuse l'accès, l'accès est accordé à l'utilisateur
Comprendre la chaîne d'évaluateurs
Le système de sécurité utilise un modèle de chaîne de responsabilité où les évaluateurs sont traités dans l'ordre de priorité. Les évaluateurs peuvent soit prendre des décisions terminales, soit déléguer à la chaîne pour combiner plusieurs vérifications.
Comment fonctionne la chaîne
- Les évaluateurs sont triés par priorité (les numéros les plus bas d'abord)
- Pour chaque évaluateur,
supports(routeClass)est appelé pour vérifier si cela s'applique - Si
supports()retournetrue, la méthodeevaluate()de l'évaluateur est appelée - L'évaluateur peut soit :
- Retourner une décision terminale (
grant()oudeny()) - arrête la chaîne - Déléguer à la chaîne en appelant
chain.evaluate()- permet à d'autres évaluateurs de s'exécuter
- Retourner une décision terminale (
- Si la chaîne se termine sans décision et que l'option sécurisé par défaut est activée, les utilisateurs non authentifiés sont refusés
Décisions terminales
Arrêtez la chaîne immédiatement :
RouteAccessDecision.grant()
- Accorde l'accès et arrête l'évaluation ultérieure
- Utilisé par
@AnonymousAccesset@PermitAll- ce sont des autorisations complètes qui ne se combinent pas avec d'autres vérifications
RouteAccessDecision.deny(reason)
- Refuse l'accès et arrête l'évaluation ultérieure
- Utilisé par
@DenyAllet lorsque les vérifications personnalisées échouent - Exemple :
RouteAccessDecision.deny("Vous ne pouvez accéder qu'à vos propres ressources")
RouteAccessDecision.denyAuthentication()
- Redirige vers la page de connexion
- Utilisé lorsque l'authentification est requise mais manquante
Délégation de la chaîne
Permet de combiner les vérifications :
chain.evaluate(routeClass, context, securityContext)
- Passe le contrôle au prochain évaluateur de la chaîne
- Permet de combiner plusieurs vérifications d'autorisation
- Utilisé par
@RolesAllowedet@RouteAccessaprès que leurs vérifications aient réussi - Les évaluateurs personnalisés doivent utiliser ce modèle lorsque les vérifications réussissent pour permettre la composition
Priorité des évaluateurs
Les évaluateurs sont vérifiés dans l'ordre de priorité (les numéros les plus bas d'abord). Les évaluateurs du cadre utilisent des priorités de 1 à 9, les évaluateurs personnalisés doivent utiliser 10 ou plus.
Les évaluateurs intégrés sont enregistrés dans cet ordre :
// Priorité 1 : @DenyAll - bloque tout
// Priorité 2 : @AnonymousAccess - autorise l'accès non authentifié
// Priorité 3 : AuthenticationRequiredEvaluator - assure l'authentification pour @PermitAll/@RolesAllowed
// Priorité 4 : @PermitAll - nécessite uniquement l'authentification
// Priorité 5 : @RolesAllowed - nécessite des rôles spécifiques
// Priorité 6 : @RouteAccess - expressions SpEL (Spring Security uniquement)
// Priorité 10+ : Évaluateurs personnalisés (comme @RequireOwnership)
Comment la priorité affecte l'évaluation
- Les évaluateurs de priorité inférieure s'exécutent en premier et peuvent "court-circuiter" la chaîne
@DenyAll(priorité 1) s'exécute en premier - si présent, l'accès est toujours refusé@AnonymousAccess(priorité 2) s'exécute ensuite - si présent, l'accès est toujours accordé (même sans authentification)AuthenticationRequiredEvaluator(priorité 3) vérifie si la route nécessite une authentification et si l'utilisateur est authentifié- Si aucun évaluateur ne gère la route, la logique sécurisé par défaut s'applique
Définir la priorité
Définissez la priorité avec l'annotation @RegisteredEvaluator :
@RegisteredEvaluator(priority = 10) // S'exécute après les évaluateurs intégrés
public class OwnershipEvaluator implements RouteSecurityEvaluator {
// ...
}
Les évaluateurs personnalisés doivent utiliser une priorité de 10 ou plus. Les priorités de 1 à 9 sont réservées aux évaluateurs du cadre. Si vous utilisez une priorité dans la plage réservée, vous recevrez un avertissement dans les journaux.
Combinaison d'évaluateurs
Les évaluateurs qui délèguent à la chaîne peuvent être combinés pour créer une logique d'autorisation complexe. Les routes peuvent avoir plusieurs annotations de sécurité :
Combinaison de vérifications de rôle avec une logique personnalisée
@Route("/users/:userId/settings")
@RolesAllowed("USER")
@RequireOwnership("userId")
public class UserSettingsView extends Composite<Div> {
// Doit avoir le rôle USER ET accéder à ses propres paramètres
}
Comment ça fonctionne :
RolesAllowedEvaluator(priorité 5) vérifie si l'utilisateur a le rôle "USER"- Si oui, appelle
chain.evaluate()pour continuer OwnershipEvaluator(priorité 10) vérifie siuserIdcorrespond à celui de l'utilisateur connecté- Si oui, appelle
chain.evaluate()pour continuer - La chaîne se termine → accès accordé
Combinaison d'expressions SpEL avec une logique personnalisée
@Route("/admin/users/:userId/edit")
@RouteAccess("hasRole('ADMIN')")
@RequireOwnership("userId")
public class AdminEditUserView extends Composite<Div> {
// Doit être administrateur ET accéder à son propre compte
}
Ce qui ne peut pas être combiné
@AnonymousAccess et @PermitAll prennent des décisions terminales - elles accordent immédiatement l'accès sans appeler la chaîne. Vous ne pouvez pas les combiner avec des évaluateurs personnalisés :
// @PermitAll accorde l'accès immédiatement, @RequireOwnership ne s'exécute jamais
@Route("/users/:userId/profile")
@PermitAll
@RequireOwnership("userId")
public class ProfileView extends Composite<Div> {
// ...
}
Pour les ressources auxquelles tous les utilisateurs authentifiés peuvent accéder, utilisez @RolesAllowed avec un rôle commun à la place :
// @RolesAllowed délègue à la chaîne
@Route("/users/:userId/profile")
@RolesAllowed("USER")
@RequireOwnership("userId")
public class ProfileView extends Composite<Div> {
// Doit être un utilisateur authentifié ET accéder à son propre profil
}