Spring Security

Authentification et authorization avec Spring Security

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Commencé en 2003, il est l'un des projets les murs du Spring. Il permet de gérer l'authentification et d'autorisation des utilisateurs dans l'application développée sous ce framework. Avec sa simplicité de configuration via les fichiers XML, il peut être déployé très facilement.

Cette description rend bien la spécificité du Spring Security, un projet qui doit implémenter la partie d'authentification et d'autorisation (ACL) dans les applications Spring. A travers cet article on verra comment utiliser ce framework. On abordera des concepts bien connus du monde du web : connexion, hiérarchie de rôles ainsi que la gestion de sessions.

Mettre en place Spring Security

L'installation du Spring Security dans une application Spring est simple. Elle repose sur la définition d'un fichier de configuration de ce premier framework. Le fichier doit être ensuite rajouté dans la liste des fichiers de configuration lus pendant l'initialisation du contexte. La configuration comprend les URLs vers les formulaires, la définition des rôles, la gestion des sessions et de gestionnaires d'autorisation. On décrira chacune de ces parties plus loin dans l'article.

Il y a encore un élément important à rajouter pour faire fonctionner Spring Security. Il s'agit d'un filtre de sécurité qu'on doit préciser dans notre web.xml :

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

DelegatingFilterProxy est une classe Spring qui crée automatiquement un bean accessible dans le contexte. Le bean porte le même nom que le filtre spécifié dans web.xml. La classe du bean doit implémenter l'interface javax.servlet.Filter. Le code ci-dessus indique donc que toutes les requêtes (/*) seront traitées par le bean qui s'appelle springSecurityFilterChain.

Maintenant quand on sait comment installer Spring Security dans notre projet, il est temps d'aborder ce framework plus en détails.

Connexion dans Spring Security

La gestion d'authentification sous Spring Security se fait avec l'élément <http />. Ce fragment permet de spécifier les URLs spécifiques auxquels on peut accéder uniquement avec un rôle déterminé (intercept-url). Les autres balises définissent les pages de connexion ou de déconnexion ainsi que la gestion des utilisateurs connectés. Regardons un exemple de configuration d'authentification du Spring Security :

    <http authentication-manager-ref="frontend" auto-config="true" use-expressions="true">
        <intercept-url pattern="/logout" access="ROLE_USER" />
        <form-login login-page="/login" default-target-url="/account/modify-password" 
        authentication-failure-url="/login?error=true" password-parameter="password" 
        username-parameter="login" />
        <remember-me user-service-ref="userDetailsServiceImpl" data-source-ref="dataSource" key="remMeK_00@"  />
        <session-management invalid-session-url="/" session-fixation-protection="newSession">
            <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </session-management>
        <logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" delete-cookies="JSESSIONID" />
        <access-denied-handler error-page="/access-denied"/>
    </http>

Voici les éléments compris dans le tag http :
- <intercept-url /> : définit une URL qui nécessite un rôle précis pour pouvoir y accéder. En occurrence, pour accéder au /logout, on doit être un utilisateur connecté (rôle ROLE_USER).
- <form-login /> : détermine toute la configuration de la page de connexion. Dans l'attribut login-page on précise l'URL de la page de connexion. default-target-url indique la page à laquelle l'utilisateur sera redirigée après une connexion réussie. authentication-failure-url contient l'URL de la page d'erreur de connexion. Ensuite les deux paramètres (password-paramter et username-parameter) indiquent les paramètres de la requête avec, le mot de passe et le login.
- <remember-me /> : définit le comportement pour la gestion de la fonctionnalité "se souvenir de moi". Cette partie sera abordée plus loin.
- <session-management /> : précise la configuration des sessions. Cette partie sera également traitée dans un nouveau paragraphe.
- <logout /> : pareil que dans form-login, mais ce tag renseigne toute la configuration sur la page de déconnexion. Un attribut spécifique, delete-cookies indique que le cookie JSESSIONID sera supprimé à la déconnexion. Le paramètre invalidate-session veut dire si la session doit être invalidée à la fin. Par défaut sa valeur est true. Cependant, dans certains cas, on peut vouloir garder la session valide. - <access-denied-handler error-page="/access-denied"/> : il s'agit d'un paramètre qui détermine l'URL de la page qui s'affichera quand l'utilisateur n'aura pas suffisamenent de droits pour y accéder. Dans notre cas, ce sera /access-denied qui lui sera présenté dans cette situation.

Se souvenir de moi dans Spring Security

La fonctionnalité "se souvenir de moi" se cache sur la balise <remember-me />. Le principe de fonctionnement est simple. Si la case "se souvenir de moi" (appelé "_spring_security_remember_me" dans le code) est cochée, un cookie est envoyé et stocké chez l'utilisateur. Maintenant, après sa nouvelle visite sur le site, il sera automatiquement authentifié. Regardons encore le code :

<remember-me user-service-ref="userDetailsServiceImpl" data-source-ref="dataSource" key="remMeK_00@"  />

Le premier attribut, user-service-ref, définit le service qui doit être utilisé pour gérer les connexions. Sa précision est obligatoire si l'on gère plusieurs connexion (par exemple vers le backoffice et le frontoffice). L'attribut data-source-ref indique le bean contenant la configuration de la base de données. Grâce à lui, la table persistent_logins stockant les données des utilisateurs auto-connectés, est créée. Concernant le paramètre key, il contient le clé privée qui protège contre les modifications locales du token "se souvenir de moi".

L'user-service-ref se présente de la manière suivante :

afficher le code

Ce service implémente l'interface org.springframework.security.core.userdetails.UserDetailsService. Cette implémentation surcharge la seule méthode de cette interface, loadUserByUsername() qui récupère l'instance UserDetails grâce à l'identifiant renseigné. Ensuite il lui attribue les rôles sur la base du champ role. Ensuite on crée l'objet UserDetails en déterminant s'il s'agit d'un utilisateur confirmé et non blacklisté. Les deux setters suivants sont spécifiques à notre implémentation. Ils précisent l'id de l'utilisateur et le nombre des livres empruntés.

Comment Spring Security sait que le mot de passe saisi est correct ? La méthode loadUserByUsername recherche l'instance UserDetails uniquement à travers l'identifiant renseigné. Ensuite on compare l'instance créée à partir des données d'authentification renseignées sur la page de connexion avec l'instance UserDetails créée par cette méthode.

Gestionnaires d'authentification

Les gestionnaires d'authentification, authentication-manager dans notre fichier de configuration, précisent toute la configuration qui doit être utilisée dans le processus d'authentification de chaque balise <http />. Dans l'exemple précédent il s'agit de l'attribut authentication-manager-ref de cette balise.

La balise authentication-manager se présente ainsi :

    <authentication-manager id="frontend">
        <authentication-provider user-service-ref="userDetailsServiceImpl">
            <password-encoder ref="passwordEncoder">
                <salt-source ref="saltCellar" />
            
        </authentication-provider>
    </authentication-manager>

Ce code est plutôt simple à comprendre. Tout d'abord, on définit le gestionnaire avec l'attribut id. Cet attribut est ensuite utilisé dans le paramètre authentication-manager-ref du <http />. Toute la configuration crée l'instance de la classe org.springframework.security.authentication.ProviderManager.

Concernant la balise suivante, <authenciation-provider, il s'agit d'un fournisseur des données d'authentification. C'est lui qui va récupérer l'utilisateur de la base de données et comparer ses valeurs (le login et le mot de passe) avec celles saisies dans le formulaire de connexion. La précision du service à utiliser peut se faire avec un des 2 attributs : jdbc-user-service ou user-service-ref.

Un seul enfant de la balise authentication-provider, <password-encoder, définit la classe utilisée pour l'encodage du mot de passe. Il contient un enfant, <salt-source. Cette balise définit, grâce à l'attribut ref, le bean qui se chargera de "saler" le mot de passe. Le concept du salage du mot de passe dans les applications web ne sera pas abordé ici en détails. Juste pour rappeler, il s'agit de rajouter des caractères supplémentaires au mot de passe saisi par l'utilisateur afin de le rendre plus difficile à deviner par un utilisateur malveillant.

Voici la page de connexion utilisée pour gérer l'authentification :

<form action="" method="POST">
 
		<table>
			<tr>
				<td>User:</td>
				<td><input type="text" name="login" value="">
				</td>
			</tr>
			<tr>
				<td>Password:</td>
				<td><input type="password" name="password" />
				</td>
			</tr>
			<tr>
<input type="checkbox" name="_spring_security_remember_me"  />Remember me
				<td colspan="2"><input name="submit" type="submit"
					value="submit" />
				</td>
			</tr>
			<tr>
				<td colspan="2">
				</td>
			</tr>
		</table>
	</form>

// TODO : changer en un formulaire UI plus propre

Gestion des sessions sous Spring Security

La balise <http /> permet aussi de définir la gestion des sessions sour Spring. Cela se fait grâce à la balise <session-management />, comme dans l'exemple suivant :

        <session-management invalid-session-url="/" session-fixation-protection="newSession">
            <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </session-management>

Les attributs de cette balise garantissent une meilleure protection de l'utilisateur contre les attaques visant la session. Un article de mon blog présente les les bonnes pratiques de gestion des sessions.

Le premier attribut de la balise session-management, invalid-session-url, indique l'URL qui est appelé quand la session est invalide.

Le deuxième attribut, session-fixation-protection, aide à protéger notre application contre les attaques du type session fixation<. Il s'agit des problèmes où un utilisateur malveillant crée la session manuellement sur le site et l'envoi (sous forme d'URL le plus souvent) à sa victime. Trois paramètres existent pour protéger contre cette attaque :
- migrateSession : une nouvelle session est créée pour la victime. La session contient les paramètres de l'ancienne session (celle de l'utilisateur malveillant)
- none : ne fait rien, la session de l'utilisateur malveillant est gardée
- newSession : une nouvelle session est créée au propre. Aucune copie des données n'est effectuée.

L'enfant de la balise session-management concerne l'accès concurrentiel d'un utilisateur au système. <concurrency-control /> permet de spécifier combien de sessions actives ouvertes dans son navigateur peut avoir un utilisateur. L'attribut max-sessions spécifie ce nombre tandis qu'error-if-maximum-exceeded indique si l'exception SessionAuthenticationException doit être lancée quand ce nombre sera dépassé.

Gestion des rôles dans Spring Security

Dans la partie intercept-url de la balise http on a vu un attribut qu'on n'a pas encore expliqué. Il s'agit de l'access. Il se charge d'établir une liste des rôles qui peuvent accéder à une ressource donnée.

Comment sont définis les rôles d'un utilisateur ? Pour le savoir, on doit revenir vers notre UserDetailsService où l'on retrouve le fragment suivant :

            // ...
            Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new GrantedAuthorityImpl(subscriber.getRole()));
            AuthenticationFrontendUserDetails userDetails = new AuthenticationFrontendUserDetails(subscriber.getLogin(), subscriber.getPassword(),
                                               subscriber.ifConfirmed(),
                                                true, true, !subscriber.ifBlacklisted(), 
                                                authorities
                                               );
            // ...

Les parties importantes de ce code sont : la création d'une collection contenant les instances de la classe GrantedAuthority et son rajout dans le constructeur AuthenticationFrontendUserDetails. Il s'agit donc d'une liste d'autorités (rôles) associées à l'utilisateur connecté.

Les rôles sont un élément de base d'ACL. Ils permettent de définir les ressources auxquelles peut accéder un utilisateur dans une application basée sur les rôles différents. Dans notre cas, le backoffice implémentera ce mécanisme. On voudra par exemple, qu'un utilisateur puisse gérer uniquement les comptes des clients et qu'un autre puisse uniquement manipuler le stock des livres pouvant être empruntés.

Annotations d'autorisation dans Spring

Maintenant quand on a découvert les fonctionnalités importantes du Spring Security, on doit savoir comment l'implémenter dans le code de nos contrôleurs et des services. Pour ce faire on peut soit utiliser l'attribut access de la balise intercept-url, soit utiliser les annotations. On a déjà vu l'utilisation de cette première solution. Passons maintenant à la deuxième.

Pour savoir si un utilisateur connecté peut "exécuter" une méthode du service ou du contrôleur, on utilisera l'annotation @PreAuthorize. Cette annotation peut appeler des méthodes d'autorisation différentes qu'on découvrira maintenant à travers des bouts de code issus de notre application :

    @PreAuthorize("isAnonymous()")
    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String register(@ModelAttribute("subscriber") Subscriber subscriber, Model layout, RedirectAttributes redAtt, HttpServletRequest request) {
        // ...	
    }	

La méthode isAnonymous() vérifie si l'utilisateur n'est pas connecté. En occurrence, on veut que seulement l'utilisateur pas connecté puisse accéder au formulaire d'enregistrement.

    @PreAuthorize("hasRole('ROLE_USER')")
    @RequestMapping(value = "/account/modify", method = RequestMethod.GET)
    public String modifyAccount(@ModelAttribute("subscriber") Subscriber subscriber, @LoggedUser AuthenticationFrontendUserDetails user, Model layout, HttpServletRequest request) {
        // ...
    }

Ici on utilise la fonction hasRole(). Elle s'assure que l'utilisateur connecté possède un rôle ROLE_USER. Si c'est le cas, la méthode du contrôleur sera exécutée. C'est logique car la fonction en question affichera le formulaire d'édition des données de l'utilisateur authentifié.

    @PreAuthorize("(#subscriber.login == principal.username) or hasRole('ROLE_ADMIN')")
    public void updateEmail(Subscriber subscriber) {
        subscriberRepository.updateEmail(subscriber.getEmail(), subscriber.getId());
    }

Cette méthode utilise Spring Expression Language (SpEL). Le fragment #subsriber.login se réfère à l'instance de la classe Subscriber passée en paramètre de cette méthode. Concernant principal, il s'agit de l'instance de l'UserDetails. Le mot or signale l'exclusion; pour que la méthode du service puisse être exécutée, soit la condition de gauche doit être vraie, soit celle de droite.

    @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'WRITER_ADD')")
    public Writer save(Writer writer) {
        logger.info("Saving writer " + writer);
        return writerRepository.save(writer);
    }

La méthode hasAnyRole liste des rôles qui peuvent accéder à la méthode. Dans notre cas, les rôles admis pour pouvoir modifier un écrivain, sont ROLE_ADMIN et WRITER_ADD.

Une autre annotation, @PostAuthorize, permet d'effectuer l'action de validation après l'exécution d'une méthode. Cependant, on ne va pas l'employer dans notre application.

Récupérer l'utilisateur connecté dans Spring

Dans certains endroits de notre application on a besoin de récupérer les données de l'utilisateur connecté. En ce qui concerne la partie de vue, on peut l'achever grâce à la librairie des tags mise à disposition avec Spring Security. La récupération se fera alors <sec:authentication property="principal.username"/>. Cependant, la récupération dans le contrôleur est un peu plus chronophage. Elle nécessite la création d'un resolver.

Cependant, cette notion a déjà été expliquée dans la partie consacrée aux annotations personnalisées dans les contrôleurs. On va seulement rappeler le code utilisé :

afficher le code

Grâce à cela on peut récupérer l'instance UserDetails grâce à un seul paramètre passé dans la méthode. Il doit être annoté avec @LoggedUser.

Bartosz KONIECZNY Sécurité

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Comment personnaliser l'affichage des champs du formulaire ?

Pour personnaliser l'affichage des champs du formulaire sous Symfony2 on doit surcharger le paramètre nommé templating.helper.form.class. Par défaut il s'agit de la classe Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper. Le code se présente ainsi :

<?xml version="1.0" encoding="UTF-8" ?>
  <container xmlns="http://symfony.com/schema/dic/services"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <parameters>
      <parameter key="templating.helper.form.class">Frontend\FrontBundle\Helper\FormHelper
    </parameters>
  </container>
La classe surchargée va, avant d'afficher le champ, faire appel à la méthode setTheme(). Tout cela pour déterminer quel thème doit utiliser Symfony2 pour le rendu du formulaire (champ text, boutons radio etc.). Le code de cette FormHelper se présente ainsi :
namespace Frontend\FrontBundle\Helper;

use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper as ParentHelper;

class FormHelper extends ParentHelper
{
    protected $engine;

    protected $varStack;

    protected $context;

    protected $resources;

    protected $themes;

    protected $templates;

    protected function renderSection(FormView $view, $section, array $variables = array())
    {
        // if new theme is set (for exemple for override form templates)
        if(isset($variables['attr']['theme'])) $this->setTheme($view,  $variables['attr']['theme']);
        return parent::renderSection($view, $section, $variables);
    }

}
Afin de personnaliser un champ du formulaire sous Symfony2 il faut passer un paramètre theme qui appelera la fonction setTheme. Voici un exemple :
// it will show the template located ad /app/Resources/views directory
echo $view['form']->widget($form['replyType'], array('attr' => array('theme' => array(0 => ':'))));
Pour voir comment on peut personnaliser ces champs, veuillez vous référer aux fichiers placés dans /vendor/Symfony/Bundle/FrameworkBundle/Resources/views/Form.