Custom AccessDecisionVoters dans Spring Security

1. Introduction

La plupart du temps, lors de la sécurisation d’une application Web Spring ou d’une API REST, les outils fournis par Spring Security sont largement suffisants, mais nous recherchons parfois un comportement plus spécifique.

Dans ce tutoriel, nous allons écrire un AccessDecisionVoter personnalisé et montrer comment il peut être utilisé pour extraire la logique d’autorisation d’une application Web et la séparer de la logique métier de l’application.

2. Scénario

Pour illustrer le fonctionnement de AccessDecisionVoter , nous allons implémenter un scénario avec deux types d’utilisateur USER et ADMIN, dans lequel un USER peut accéder au système uniquement pendant des minutes paires, tandis qu’un ADMIN sera toujours autorisé.

3. AccessDecisionVoter Implémentations

Dans un premier temps, nous allons décrire quelques-unes des implémentations fournies par Spring qui participeront avec notre électeur coutumier à la décision finale concernant l’autorisation. Nous verrons ensuite comment implémenter un votant personnalisé.

3.1. Les implémentations AccessDecisionVoter par défaut

Spring Security fournit plusieurs implémentations de AccessDecisionVoter .

Nous en utiliserons quelques uns dans le cadre de notre solution de sécurité ici.

Voyons comment et quand les implémentations par défaut des électeurs voteront.

Le AuthenticatedVoter émettra un vote basé sur le niveau d’authentification de l’objet Authentication - recherchant spécifiquement un prédécessal entièrement authentifié, un authentifié avec Remember-me ou, enfin, anonyme.

Le RoleVoter vote si l’un des attributs de configuration commence par la chaîne «ROLE » . Si c’est le cas, il recherchera le rôle dans la liste GrantedAuthority de l’objet Authentication__.

WebExpressionVoter nous permet d’utiliser SpEL (Spring Expression Language) pour autoriser les requêtes à l’aide de l’annotation @ PreAuthorize .

Par exemple, si nous utilisons Java config:

@Override
protected void configure(final HttpSecurity http) throws Exception {
    ...
    .antMatchers("/").hasAnyAuthority("ROLE__USER")
    ...
}

Ou en utilisant une configuration XML - nous pouvons utiliser SpEL dans une balise intercept-url , dans la balise http :

<http use-expressions="true">
    <intercept-url pattern="/"
      access="hasAuthority('ROLE__USER')"/>
    ...
</http>

3.2. Implémentation personnalisée de AccessDecisionVoter

Créons maintenant un voteur personnalisé - en implémentant l’interface AccessDecisionVoter :

public class MinuteBasedVoter implements AccessDecisionVoter {
   ...
}

La première des trois méthodes que nous devons fournir est la méthode vote . La méthode vote est la partie la plus importante de l’électeur personnalisé et correspond à la logique d’autorisation.

La méthode vote peut renvoyer trois valeurs possibles:

  • ACCESS GRANTED__ - l’électeur donne une réponse affirmative

  • ACCESS DENIED__ - l’électeur donne une réponse négative

  • ACCESS ABSTAIN__ - l’électeur s’abstient de voter

Implémentons maintenant la méthode vote :

@Override
public int vote(
  Authentication authentication, Object object, Collection collection) {
    return authentication.getAuthorities().stream()
      .map(GrantedAuthority::getAuthority)
      .filter(r -> "ROLE__USER".equals(r)
        && LocalDateTime.now().getMinute() % 2 != 0)
      .findAny()
      .map(s -> ACCESS__DENIED)
      .orElseGet(() -> ACCESS__ABSTAIN);
}

Dans notre méthode vote , nous vérifions si la requête provient d’un USER . Si tel est le cas, nous renverrons ACCESS GRANTED s’il s’agit d’une minute paire, sinon nous renverrons ACCESS DENIED. Si la demande ne provient pas d’un USER, nous nous abstenons du vote et nous retournons ACCESS ABSTAIN__.

La deuxième méthode indique si l’électeur prend en charge un attribut de configuration particulier. Dans notre exemple, l’électeur n’a besoin d’aucun attribut de configuration personnalisé. Nous renvoyons donc true :

@Override
public boolean supports(ConfigAttribute attribute) {
    return true;
}

La troisième méthode indique si l’électeur peut voter pour le type d’objet sécurisé ou non. Puisque notre votant n’est pas concerné par le type d’objet sécurisé, nous retournons true :

@Override
public boolean supports(Class clazz) {
    return true;
}

4. Le AccessDecisionManager

La décision d’autorisation finale est gérée par le AccessDecisionManager .

Le AbstractAccessDecisionManager contient une liste de __AccessDecisionVoter __s - chargés de voter indépendamment les uns des autres.

Il existe trois implémentations pour le traitement des votes afin de couvrir les cas d’utilisation les plus courants:

  • AffirmativeBased - accorde l’accès si l’un des

_AccessDecisionVoter s renvoient un vote affirmatif ** ConsensusBased_ - accorde l’accès s’il y a plus de votes affirmatifs

que négatif (en ignorant les utilisateurs qui s’abstiennent) ** UnanimousBased__ - accorde l’accès si chaque électeur s’abstient ou

renvoie un vote affirmatif

Bien sûr, vous pouvez implémenter votre propre AccessDecisionManager avec votre logique de prise de décision personnalisée.

5. Configuration

Dans cette partie du didacticiel, nous examinerons les méthodes basées sur Java et XML pour configurer notre AccessDecisionVoter personnalisé avec un AccessDecisionManager .

5.1. Configuration Java

Créons une classe de configuration pour Spring Web Security:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}

Et définissons un bean AccessDecisionManager qui utilise un gestionnaire UnanimousBased avec notre liste personnalisée de votants:

@Bean
public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<? extends Object>> decisionVoters
      = Arrays.asList(
        new WebExpressionVoter(),
        new RoleVoter(),
        new AuthenticatedVoter(),
        new MinuteBasedVoter());
    return new UnanimousBased(decisionVoters);
}

Enfin, configurons Spring Security pour utiliser le bean défini précédemment comme valeur par défaut AccessDecisionManager :

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    ...
    .anyRequest()
    .authenticated()
    .accessDecisionManager(accessDecisionManager());
}

5.2. Configuration XML

Si vous utilisez la configuration XML, vous devrez modifier votre fichier spring-security.xml (ou le fichier contenant vos paramètres de sécurité).

Tout d’abord, vous devrez modifier la balise <http> :

<http access-decision-manager-ref="accessDecisionManager">
  <intercept-url
    pattern="/** ** "
    access="hasAnyRole('ROLE__ADMIN', 'ROLE__USER')"/>
  ...
</http>

Ensuite, ajoutez un haricot pour l’électeur personnalisé:

<beans:bean
  id="minuteBasedVoter"
  class="org.baeldung.voter.MinuteBasedVoter"/>

Ajoutez ensuite un bean pour le AccessDecisionManager :

<beans:bean
  id="accessDecisionManager"
  class="org.springframework.security.access.vote.UnanimousBased">
    <beans:constructor-arg>
        <beans:list>
            <beans:bean class=
              "org.springframework.security.web.access.expression.WebExpressionVoter"/>
            <beans:bean class=
              "org.springframework.security.access.vote.AuthenticatedVoter"/>
            <beans:bean class=
              "org.springframework.security.access.vote.RoleVoter"/>
            <beans:bean class=
              "org.baeldung.voter.MinuteBasedVoter"/>
        </beans:list>
    </beans:constructor-arg>
</beans:bean>

Voici un exemple de balise <authentication-manager> prenant en charge notre scénario:

<authentication-manager>
    <authentication-provider>
        <user-service>
            <user name="user" password="pass" authorities="ROLE__USER"/>
            <user name="admin" password="pass" authorities="ROLE__ADMIN"/>
        </user-service>
    </authentication-provider>
</authentication-manager>

Si vous utilisez une combinaison de configurations Java et XML, vous pouvez importer le fichier XML dans une classe de configuration:

@Configuration
@ImportResource({"classpath:spring-security.xml"})
public class XmlSecurityConfig {
    public XmlSecurityConfig() {
        super();
    }
}

6. Conclusion

Dans ce didacticiel, nous avons étudié un moyen de personnaliser la sécurité d’une application Web Spring à l’aide de _AccessDecisionVoter s. Nous avons vu des électeurs fournis par Spring Security qui ont contribué à notre solution. Nous avons ensuite expliqué comment implémenter un AccessDecisionVoter_ personnalisé.

Ensuite, nous avons discuté de la façon dont le AccessDecisionManager prend la décision d’autorisation finale et nous avons montré comment utiliser les implémentations fournies par Spring pour prendre cette décision après que tous les électeurs aient voté.

Ensuite, nous avons configuré une liste de AccessDecisionVoters avec un AccessDecisionManager via Java et XML.

L’implémentation est disponible dans le projet Github

Lorsque le projet est exécuté localement, la page de connexion est accessible à l’adresse suivante:

http://localhost:8080/spring-security-custom-permissions/login

Les informations d’identification pour USER sont «utilisateur» et «pass» et les informations d’identification pour ADMIN sont «admin» et «pass».