1. Quand les rôles ne suffisent pas
Si vous regardez les rôles uniquement avec les yeux d'un débutant, ils ressemblent à une solution idéale : courts, compréhensibles, presque comme des badges lors d'une conférence. Mais un projet réel commence rapidement à poser des questions gênantes. Par exemple : « L'éditeur peut voir la file d'attente de vérification — mais seul l'éditeur senior peut publier », ou « L'administrateur peut lire les utilisateurs — mais tous les administrateurs ne peuvent pas changer les rôles ». Et si vous essayez d'exprimer tout cela uniquement avec des rôles, vous commencerez rapidement à multiplier des entités comme SUPER_ADMIN_OF_THE_UNIVERSE, ce qui semble amusant jusqu'à la première revue.
Dans notre projet, cela se manifeste très naturellement. Nous avons toute une zone /api/editor/**, et à l'intérieur différentes actions : voir la file d'attente, rejeter un matériau, publier un matériau. Si nous disons « tout cela est accessible au rôle EDITOR », nous offrons automatiquement les mêmes pouvoirs à tous les éditeurs. Et si demain il faut diviser les droits — il faudra soit modifier le modèle de rôle (en cassant les décisions précédentes), soit ajouter soigneusement des droits précis.
C'est ici que devient utile le deuxième niveau d'expressivité : authority comme permission distincte et précise sur une action.
Modèle : rôle — zone, authority — action
Il est important de ne pas se noyer dans les termes, donc nous allons conserver une logique simple. Le rôle répond à la question : « À quelle classe d'utilisateurs appartient la personne ? » ou « Dans quelle zone est-elle généralement admise ? ». C'est une étiquette grossière. L'authority répond à la question : « Quelle action spécifique lui est autorisée ? ». C'est un droit précis. Dans un bon système, ils ne sont pas en concurrence, mais se complètent : le rôle donne un domaine d'accès de base, l'authority précise les opérations spécifiques à l'intérieur de ce domaine.
Imaginez un bureau. Le rôle est un badge pour l'étage : « employé », « éditeur », « administrateur ». L'authority est le droit d'appuyer sur un bouton spécifique : « peut imprimer des documents », « peut signer des contrats », « peut éteindre le serveur (s'il vous plaît, non) ». Si vous essayez de créer un rôle pour chaque bouton, vous aurez 200 rôles et un développeur malheureux qui les maintient. Généralement, ce développeur, c'est vous dans le futur.
Fixons la différence dans un tableau compact :
| Caractéristique | Role (rôle) | Authority (droit) |
|---|---|---|
| Sens | « zone/classe d'utilisateur » | « action spécifique » |
| Granularité | grossière | précise |
| Bons exemples | USER, , |
draft:publish, , |
| Où il est pratique de lire | « carte de l'application » dans SecurityFilterChain | règles spécifiques sur des endpoint particuliers |
| Principal danger | droits trop larges « par défaut » | fouillis de chaînes et fautes de frappe |
2. Vérifications : hasRole(...) et hasAuthority(...)
Lorsque nous écrivons des règles d'accès dans SecurityFilterChain, Spring Security vérifie en fin de compte : « l'utilisateur actuel a-t-il le droit nécessaire ou non ? ». Le rôle et l'authority sont tous deux vérifiés à travers le même concept — l'ensemble des droits de l'utilisateur. La différence réside dans la façon dont nous nommons ces droits et dont nous les vérifions : en « mode rôle » ou en « mode chaîne exacte ».
Au niveau de la configuration, cela ressemble généralement à ceci : nous utilisons les rôles pour les grandes zones, et les authorities pour les actions ponctuelles. Dans le projet, cela sera particulièrement clair pour les actions d'édition : « voir la file d'attente » et « publier » sont des droits différents, bien que les deux appartiennent à la partie éditeur.
Un petit extrait de configuration : ici, la différence entre zone et action ponctuelle est importante, sans entrer dans les détails de la façon dont les rôles se présentent ensuite sous forme de chaînes d'authority :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> auth
// Nous utilisons le rôle comme "marqueur de zone" : nous autorisons l'accès à l'endpoint spécifique des éditeurs.
.requestMatchers("/api/editor/review-queue").hasRole("EDITOR")
// Nous utilisons l'authority comme une action ponctuelle : la publication nécessite un droit explicite.
.requestMatchers("/api/editor/drafts/*/publish").hasAuthority("draft:publish")
// Nous refusons toutes les autres requêtes dans cette configuration par défaut.
.anyRequest().denyAll()
).build();
}
}
Le sens : pour voir la file d'attente, nous « laissons entrer les éditeurs », et pour l'action spécifique de publication, nous exigeons un droit explicite draft:publish. Cela rend déjà le modèle d'accès « plus mature » : le rôle reste une carte pratique, l'authority ajoute de la précision là où le rôle serait trop grossier.
Il est important de noter au préalable un point pratique : si vous avez à la fois un matcher général (par exemple, /api/editor/**) et un plus précis (par exemple, /api/editor/drafts/*/publish), alors le plus précis doit être placé en premier. Ce n'est pas un nouveau sujet (nous avons déjà parlé de l'ordre des règles), mais c'est précisément avec les authorities que vous commencez à ressentir pourquoi l'ordre n'est pas un « style » mais une logique.
3. Noms des authorities : format resource:action
Parlons maintenant de la partie la plus « discrète » mais super importante : comment nommer les authorities. Si les rôles sont généralement limités à trois à cinq valeurs et vivent des décennies, les authorities aiment se multiplier. Et si vous ne définissez pas un schéma de nommage clair, dans une semaine vous aurez un ensemble de chaînes comme CAN_PUBLISH, PUBLISH_DRAFT, DRAFT_PUBLISH, publishDraft, draftPublishPermission — et tout cela signifiera la même chose, mais sera vérifié différemment. Un excellent moyen de faire carrière dans le débogage.
Dans notre cours, nous fixons un format simple et lisible : resource:action. La ressource — « ce que nous protégeons », l'action — « ce que nous faisons ».
Par exemple :
- profile:read, profile:write
- avatar:upload
- draft:create, draft:publish
- user:read, user:manage
Un tel nom est agréable car on peut le lire dans les logs et comprendre immédiatement le sens. Et si vous voyez user:manage, vous n'avez pas besoin de deviner : « c'est pour la lecture ? la modification ? la magie ? ». C'est pour la gestion des utilisateurs. Point final.
Pour ne pas avoir à gérer les chaînes à la main à dix endroits, il est pratique de les mettre dans des constantes. Ce n'est pas une exigence obligatoire aujourd'hui, mais en tant que « discipline de projet », cela aide beaucoup :
package com.example.securecontent.security;
// Constantes pour les authorities : pour ne pas disperser les "chaînes magiques" dans le projet.
public final class Authorities {
// Nous interdisons la création d'instances : cette classe n'est qu'un "conteneur" pour les chaînes.
private Authorities() {}
// Authority pour la publication de brouillon.
public static final String DRAFT_PUBLISH = "draft:publish";
// Authority pour la gestion des utilisateurs (par exemple : changement de rôles, blocages, etc.).
public static final String USER_MANAGE = "user:manage";
}
Le sens n'est même pas dans la « beauté ». Le sens est qu'une chaîne est une chose fragile. Un espace supplémentaire — et vous obtenez un système de sécurité parfait qui « pour une raison quelconque renvoie toujours 403 ».
4. Rôles et authorities dans le projet
À ce stade du cours, nous ne construisons pas d'ACL d'entreprise, nous n'écrivons pas de moteur de politiques et nous ne faisons pas semblant d'être une banque. Nous construisons un projet pédagogique, et nous avons un bon principe de bon sens : le rôle est le langage principal des zones, l'authority est le langage des actions ponctuelles. Cela donne le meilleur équilibre entre lisibilité et précision.
Dans Secure Content Platform API, des exemples très naturels d'actions ponctuelles apparaissent dans les parties éditeur et admin. Le rôle EDITOR explique que la personne travaille généralement avec la modération. Mais « publication » est une action puissante, et il est parfois utile de la séparer en un droit distinct draft:publish. De même pour l'admin : le rôle ADMIN dit « c'est un administrateur », mais le droit user:manage montre explicitement que cet administrateur particulier peut changer les rôles, bloquer des comptes, etc.
Exemple de « carte d'accès mixte » : grandes zones sur les rôles, opérations individuelles sur les authorities. Notez l'ordre — d'abord l'action précise, puis la zone plus générale :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> auth
// Nous plaçons la règle la plus précise en premier : la publication nécessite une authority distincte.
.requestMatchers("/api/editor/drafts/*/publish").hasAuthority("draft:publish")
// Zone plus large : nous autorisons l'accès à la partie éditeur dans son ensemble par le rôle EDITOR.
.requestMatchers("/api/editor/**").hasRole("EDITOR")
// Zone admin : accès par le rôle ADMIN.
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Nous refusons le reste pour ne pas laisser accidentellement une "brèche" par défaut.
.anyRequest().denyAll()
).build();
}
}
Pourquoi est-ce pratique ? Parce que vous pouvez donner à une personne l'accès à la zone éditeur (rôle EDITOR), mais ne pas lui donner la publication (pas de draft:publish). Ainsi, elle pourra voir la file d'attente et, par exemple, laisser des commentaires (si vous le décidez), mais elle ne pourra pas appuyer sur le « gros bouton rouge » de publication. Et c'est déjà un modèle très proche de la réalité.
5. Attribution des rôles et authorities in-memory
Maintenant, une question pratique : si nous avons des authorities, comment les attribuer aux utilisateurs dans un modèle in-memory ? Il est important de se rappeler que in-memory n'est pas un « jouet », mais un modèle pédagogique. Notre objectif est d'obtenir un ensemble déterministe de comptes pour vérifier manuellement la matrice d'accès. Par conséquent, nous créons délibérément plusieurs utilisateurs « scénarisés » : un utilisateur ordinaire, un éditeur, un administrateur.
Pour un cas simple, nous pouvons utiliser roles(...), et pour un cas plus précis — authorities(...). Ici, le principe important est que dans un seul compte peuvent coexister des valeurs de type rôle et des droits ponctuels ; authorities(...) nous oblige simplement à écrire ces chaînes sans ajouts automatiques.
Ci-dessous n'est pas un nouvel ensemble final d'utilisateurs du projet. Nous avons besoin d'un extrait local : voir à quoi ressemble un compte éditeur si on stocke à la fois des chaînes de rôle et un droit ponctuel sous forme de chaînes exactes.
Exemple d'utilisateur in-memory dans le style « chaînes exactes » :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class InMemoryUsersConfig {
@Bean
InMemoryUserDetailsManager users(PasswordEncoder passwordEncoder) {
// Dans un projet pédagogique, le mot de passe doit être encodé avec le PasswordEncoder que vous avez configuré dans le contexte.
// Sinon, Spring Security ne démarrera pas ou rejettera la connexion.
UserDetails editor = User.builder()
.username("eva")
.password(passwordEncoder.encode("eva-pass"))
// Ici, nous fournissons explicitement la liste des authorities en chaînes.
// Nous définissons les rôles comme ROLE_USER / ROLE_EDITOR, car authorities(...) n'ajoute pas de préfixe pour nous.
// Et nous stockons le droit ponctuel de publication exactement comme "draft:publish".
.authorities("ROLE_USER", "ROLE_EDITOR", "draft:publish")
.build();
// Nous retournons un gestionnaire avec un ensemble déterministe d'utilisateurs pour vérifier la matrice d'accès.
return new InMemoryUserDetailsManager(editor);
}
}
Pour cette réflexion, un seul éditeur suffit : il montre comment, dans une même liste, coexistent des chaînes de rôle et un droit ponctuel.
Cet exemple est volontairement court : nous montrons le principe — un utilisateur a des « marqueurs de zone » et un droit ponctuel. Les chaînes ROLE_USER et ROLE_EDITOR sont nécessaires ici car dans authorities(...), nous écrivons les rôles sous la forme sous laquelle ils sont réellement stockés chez l'utilisateur.
Un autre point méthodologique important : dans un projet pédagogique, ne créez pas un seul « utilisateur root » qui peut tout faire. Il n'est pratique que la première heure, puis il nuit à l'apprentissage. Vous avez besoin de comptes qui montrent les limites : qui accède à la zone personnelle, qui accède à la zone éditeur, qui se heurte à un 403.
6. Erreurs typiques lors du mélange des rôles et des authorities
Erreur no1 : considérer les rôles et les authorities comme des synonymes complets.
Lorsque les rôles commencent à être utilisés comme des actions (PUBLISH, REJECT), le modèle se développe rapidement et perd son sens. Un rôle est un groupe de droits, pas une action spécifique. Substituer l'un à l'autre rend le système lourd et difficile à lire.
Erreur no2 : abandonner complètement les rôles au profit des authorities.
Essayer d'exprimer tout par des droits précis conduit à une granularité excessive. La configuration gonfle, et au lieu d'une « carte d'accès » compréhensible, on obtient un ensemble de règles non liées. Les rôles sont nécessaires précisément pour l'agrégation — afin de ne pas décrire chaque action séparément.
Erreur no3 : donner aux authorities des noms non informatifs.
Des noms comme WRITE ou DO_ACTION n'expliquent rien. Au bout d'un moment, il devient impossible de comprendre ce que ce droit autorise exactement. Le format resource:action (draft:publish, order:read) rend les droits auto-documentés et réduit la confusion.
Erreur no4 : vérifier une authority sans l'avoir attribuée à l'utilisateur.
Si la configuration contient hasAuthority("draft:publish"), mais que l'utilisateur ne possède pas ce droit, le résultat sera toujours 403. Ce n'est pas une « erreur de Spring », mais un fonctionnement correct du système. Il est important de synchroniser la vérification et la matrice de droits réelle.
Erreur no5 : appliquer de manière non systématique des vérifications de rôle et d'authority sur une même décision d'accès.
Le problème n'est pas qu'un même SecurityFilterChain contienne à la fois hasRole(...) et hasAuthority(...) — c'est un modèle mixte normal. La confusion commence lorsque pour une même action, sans raison claire, on exige à la fois un rôle et un droit précis, puis personne ne peut expliquer quel niveau est responsable de quoi. Il est préférable de se mettre d'accord à l'avance : les rôles ouvrent des zones, les authorities précisent des opérations individuelles à l'intérieur de ces zones.
GO TO FULL VERSION