Contactez-nous

Autorisation basée sur les permissions/autorités (`hasAuthority`)

Apprenez à implémenter une autorisation fine dans Spring Security en utilisant l'expression SpEL hasAuthority pour contrôler l'accès basé sur des permissions spécifiques.

Au-delà des rôles : l'autorisation fine par autorités

Si l'autorisation basée sur les rôles (RBAC) est un modèle puissant et largement utilisé, elle peut parfois manquer de granularité pour des cas d'utilisation complexes. Imaginons une application où un utilisateur peut avoir le rôle "EDITEUR" mais où seule une partie des éditeurs a le droit de "PUBLIER_ARTICLE" tandis que tous peuvent "MODIFIER_BROUILLON". Attribuer une nouvelle multitude de rôles (comme "EDITEUR_PUBLIEUR") peut vite devenir ingérable.

C'est là qu'intervient l'autorisation basée sur les permissions, ou plus techniquement dans Spring Security, basée sur les autorités. Au lieu de raisonner uniquement en termes de rôles généraux, on définit des permissions spécifiques pour des actions précises (par exemple, `READ_USER_PROFILE`, `UPDATE_PRODUCT_PRICE`, `DELETE_COMMENT`). Ces permissions sont représentées comme des `GrantedAuthority` individuelles, tout comme les rôles.

Spring Security fournit les expressions SpEL `hasAuthority()` et `hasAnyAuthority()` pour vérifier directement la présence de ces permissions spécifiques, offrant ainsi un contrôle d'accès beaucoup plus fin que le simple RBAC.

`GrantedAuthority` : l'unité de base de la permission

Comme mentionné précédemment, l'interface `org.springframework.security.core.GrantedAuthority` est la pierre angulaire de l'autorisation dans Spring Security. Chaque instance représente une permission unique accordée à l'utilisateur. La méthode clé est `String getAuthority()`, qui retourne le nom de cette permission sous forme de chaîne de caractères.

Contrairement à la convention `ROLE_` associée aux expressions `hasRole`, il n'y a pas de préfixe imposé pour les autorités vérifiées par `hasAuthority()`. Vous pouvez utiliser n'importe quelle chaîne significative pour représenter vos permissions, par exemple : `product:read`, `user:delete`, `approve_invoice`, `ACCESS_ADMIN_CONSOLE`.

L'important est la cohérence : la chaîne utilisée dans votre expression `hasAuthority()` doit correspondre exactement à la chaîne retournée par `getAuthority()` pour l'une des autorités détenues par l'utilisateur.

L'expression `hasAuthority('NOM_AUTORITE')`

L'expression `hasAuthority('X')` vérifie si l'utilisateur actuellement authentifié possède une `GrantedAuthority` dont la chaîne retournée par `getAuthority()` est exactement égale à 'X'.

Elle est idéale pour protéger des ressources ou des actions nécessitant une permission très spécifique :

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            // Accès restreint à ceux ayant l'autorité exacte 'rapport:ventes:lecture'
            .requestMatchers("/rapports/ventes/detail").hasAuthority("rapport:ventes:lecture") 
            // Accès à la suppression de produit
            .requestMatchers(HttpMethod.DELETE, "/produits/{id}").hasAuthority("produit:supprimer")
            // Un rôle peut aussi être vérifié comme une autorité si le préfixe est inclus
            .requestMatchers("/admin/audit").hasAuthority("ROLE_ADMIN") 
            // ... autres règles ...
            .anyRequest().authenticated()
        )
        // ... configuration formLogin, etc. ...
    ;
    return http.build();
}

Si un utilisateur essaie d'accéder à `/rapports/ventes/detail` sans détenir explicitement l'autorité `"rapport:ventes:lecture"`, l'accès sera refusé, même s'il possède des rôles comme `ROLE_MANAGER` ou d'autres autorités.

L'expression `hasAnyAuthority('AUTORITE1', 'AUTORITE2', ...)`

Similaire à `hasAnyRole`, l'expression `hasAnyAuthority('X', 'Y', 'Z')` vérifie si l'utilisateur possède au moins une des autorités spécifiées dans la liste. Encore une fois, la comparaison est exacte et aucun préfixe n'est ajouté.

Cela permet d'accorder l'accès si l'utilisateur détient l'une quelconque parmi plusieurs permissions pertinentes :

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            // L'utilisateur peut lire les logs s'il a l'une de ces autorités
            .requestMatchers("/system/logs/**").hasAnyAuthority("logs:read", "ROLE_ADMIN", "SUPPORT_LEVEL_2")
            // Seuls les admins peuvent effacer les logs
            .requestMatchers(HttpMethod.DELETE, "/system/logs").hasAuthority("ROLE_ADMIN")
            // ... autres règles ...
            .anyRequest().authenticated()
        )
        // ... configuration formLogin, etc. ...
    ;
    return http.build();
}

Ici, un utilisateur peut accéder à `/system/logs/view` s'il possède l'autorité `logs:read`, OU `ROLE_ADMIN`, OU `SUPPORT_LEVEL_2`.

Chargement des autorités spécifiques via `UserDetailsService`

Tout comme pour les rôles, c'est votre `UserDetailsService` qui est responsable de charger l'ensemble complet des `GrantedAuthority` de l'utilisateur, y compris ces permissions fines.

Cela implique souvent de modéliser les permissions dans votre base de données (par exemple, une table `permissions` et une table de jointure `utilisateur_permissions`) et de les récupérer lors du chargement de l'utilisateur.

@Service
public class DatabaseUserDetailsService implements UserDetailsService {
    // ... injection UserRepository, PermissionRepository ...

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = // ... charger l'utilisateur ...
                .orElseThrow(() -> new UsernameNotFoundException(...));

        // Charger les rôles (convention ROLE_)
        Set authorities = userEntity.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toSet());

        // Charger les permissions spécifiques (pas de préfixe ajouté ici)
        // Supposons que userEntity.getPermissions() retourne un Set comme {"rapport:lire", "produit:modifier"}
        Set specificPermissions = userEntity.getPermissions().stream()
                .map(permission -> new SimpleGrantedAuthority(permission)) 
                .collect(Collectors.toSet());

        // Combiner les deux types d'autorités
        authorities.addAll(specificPermissions);

        return new User(
                userEntity.getUsername(),
                userEntity.getPassword(),
                authorities // Collection complète des autorités (rôles + permissions)
        );
    }
}

Avec cette approche, l'objet `Authentication` contiendra à la fois les autorités de type rôle (avec préfixe) et les autorités de type permission (sans préfixe), permettant d'utiliser indifféremment `hasRole` et `hasAuthority` dans vos règles de sécurité.

Choisir entre `hasRole` et `hasAuthority`

Le choix entre ces deux familles d'expressions dépend de votre modèle d'autorisation :

  • Utilisez `hasRole()` / `hasAnyRole()` lorsque vous raisonnez en termes de rôles conceptuels et que vous adhérez (ou souhaitez adhérer) à la convention du préfixe `ROLE_`. C'est souvent plus lisible pour des règles générales.
  • Utilisez `hasAuthority()` / `hasAnyAuthority()` lorsque vous avez besoin d'une granularité plus fine, en vérifiant des permissions spécifiques, ou lorsque vos noms d'autorités (y compris les rôles) ne suivent pas la convention du préfixe `ROLE_`.

Il est tout à fait courant et souvent recommandé de combiner les deux approches. Par exemple, une section d'administration pourrait nécessiter `hasRole('ADMIN')`, mais une action très sensible au sein de cette section pourrait nécessiter une vérification supplémentaire avec `hasAuthority('ACTION_TRES_SENSIBLE')` en utilisant la sécurité au niveau méthode (`@PreAuthorize`).

En résumé, `hasAuthority` vous donne le contrôle le plus direct et le plus précis sur les permissions requises, vous permettant de construire des systèmes d'autorisation robustes et adaptés à des besoins métiers complexes.