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 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 d'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 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 en fonction de la logique personnalisée. Les évaluateurs sont vérifiés lors de la navigation avant que tout composant 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 les 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, voir le guide des annotations de sécurité.
Cas d'utilisation : Vérification de propriété
Une exigence courante est de 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'utilisateur ID
123est connecté - Il navigue vers
/users/456/edit - Peut-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 la route qui contient l'ID de l'utilisateur.
*/
String value() default "userId";
}
Utilisez-la sur les routes qui nécessitent 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) {
// D'abord vérifier l'authentification
if (!securityContext.isAuthenticated()) {
return RouteAccessDecision.denyAuthentication();
}
// Obtenir l'annotation
RequireOwnership annotation = routeClass.getAnnotation(RequireOwnership.class);
String paramName = annotation.value();
// Obtenir 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);
// Obtenir :userId à partir des paramètres de la route
String requestedUserId = context.getRouteParameters()
.get(paramName)
.orElse(null);
// Vérifier s'ils 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 automatiquement et enregistre les évaluateurs annotés avec @RegisteredEvaluator.
Comment cela fonctionne
L'implémentation de l'évaluateur a deux méthodes clés :
supports(Class<?> routeClass)
- Renvoie
truesi cet évaluateur doit traiter la route - Seuls les évaluateurs qui renvoient
trueseront invoqués pour la route - Filtre les routes en vérifiant la présence de 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 ID
- S'ils correspondent, délègue à
chain.evaluate()pour permettre l'exécution d'autres évaluateurs - S'ils ne correspondent pas, renvoie
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()renvoietrue(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- Renvoie
RouteAccessDecision.deny("Vous ne pouvez accéder qu'à vos propres ressources")
- L'utilisateur est redirigé vers une 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 ID correspondent → appelle
chain.evaluate()pour continuer
- Si aucun autre évaluateur ne refuse l'accès, l'utilisateur se voit accorder l'accès
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 par 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 la chaîne fonctionne
- Les évaluateurs sont triés par priorité (nombres plus bas en premier)
- Pour chaque évaluateur,
supports(routeClass)est appelé pour vérifier s'il s'applique - Si
supports()renvoietrue, la méthodeevaluate()de l'évaluateur est appelée - L'évaluateur peut soit :
- Retourner une décision terminale (
grant()oudeny()) - interrompt la chaîne - Délègue à 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 la sécurité par défaut est activée, les utilisateurs non authentifiés sont refusés
Décisions terminales
Arrêtez immédiatement la chaîne :
RouteAccessDecision.grant()
- Accorde l'accès et arrête l'évaluation supplémentaire
- 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 supplémentaire
- Utilisé par
@DenyAllet lorsque des 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 absente
Délégation de chaîne
Permet de combiner des 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 devraient utiliser ce modèle lorsque les vérifications réussissent pour permettre la composition
Priorité des évaluateurs
Les évaluateurs sont vérifiés par ordre de priorité (nombres plus bas en premier). Les évaluateurs du cadre utilisent la priorité 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 - permet l'accès anonymisé
// Priorité 3 : AuthenticationRequiredEvaluator - assure l'auth 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 - s'il est présent, l'accès est toujours refusé@AnonymousAccess(priorité 2) s'exécute ensuite - s'il est 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 traite la route, la logique de sécurité 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 1-9 sont réservées aux évaluateurs du framework. 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 à 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 immédiatement l'accès, @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
}