Spring Security

Introduction

Le module Acegi a été rebaptisé en version 2 par Spring Security. Outre ce renommage, cette nouvelle version apporte des schémas XML qui simplifient considérablement la configuration.

Spring Security permet de gérer l'accès aux ressources d'une application Java. Ces ressources peuvent être des pages web, mais aussi des objets de services métier. Toute ressource sollicitée par un appelant est rendue accessible si, d'une part, l'appelant s'est identifié, et si d'autre part, il possède les droits nécessaires (des rôles dans le vocabulaire Spring Security).

Dans cet article, nous traiterons le cas de la sécurisation des pages web.

Principe de Spring Security

Les requêtes HTTP sont interceptées par un filtre de servlet qui délègue à un bean Spring les traitements de vérification d'accès aux pages web.

SpringSecurity1.png

Ce bean met en oeuvre une chaîne de filtres. Chacun des filtres est un bean auquel est attribué une tâche précise :

SpringSecurity2.png

Certains filtres sont obligatoires, d'autres optionnels. La chaîne de filtres est largement configurable, ce qui permet de personnaliser au mieux la gestion de la sécurité dans les applications web. Spring Security offre ainsi les fonctionnalités suivantes :

Installer Spring Security

Les fichiers jar

La distribution de Spring Security fournit une bonne vingtaine de fichiers jar. Evidemment, tous ne sont pas nécessaires pour démarrer un projet. Pour les exemples qui suivent, les fichiers suivants sont suffisants :

Configurer le fichier web.xml

Il suffit de déclarer le filtre de servlet de DelegatingFilterProxy pour activer le module de sécurité :

<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>

Schéma XML

Le module Spring Security est livré avec un schéma XML spécifique. Celui-ci doit être spécifié dans les documents de configuration Spring :

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                           http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">

Une configuration de base

Voici un exemple de configuration minimale de la chaîne de filtres :

<security:authentication-provider>
  <security:user-service>
    <security:user name="admin" password="admin"
                   authorities="ROLE_ADMIN,ROLE_USER" /> 
    <security:user name="guest" password="guest"
                   authorities="ROLE_USER" /> 
  </security:user-service>
</security:authentication-provider>

<security:http>
  <security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:http-basic/>
</security:http>


Cet exemple est complet et tout à fait fonctionnel. Comparé à la version 1 d'Acegi, cela paraît surprenant car le volume de code est bien moindre, mais tout y est. Explications des éléments de cette configuration :


SpringSecurity3.png


Dans notre exemple, toutes les pages sont filtrées. La demande d'identification se produit donc quelle que soit la page sollicitée. Une fois l'utilisateur identifié, les informations de sécurité le concernant sont stockées en session HTTP dans le security context. L'identification n'est par conséquent pas demandée une nouvelle fois lorsque l'on navigue vers d'autres pages. Si l'utilisateur tente de naviguer vers une page non autorisée (par exemple editerproduit.do pour l'utilisateur guest), Spring Security renvoie une erreur 403 au navigateur web :

SpringSecurity5.png


Si l'identification de l'utilisateur est incorrecte (identifiant ou mot de passe), Spring Security renvoie une erreur 401 au navigateur web :

SpringSecurity6.png

Authentification par formulaire

Déclarer l'authentification par formulaire

Pour activer l'authentification par formulaire, il suffit de remplacer l'élément <http-basic> par l'élément <form-login> :

<security:http>
  <security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login/>
</security:http>

Quand <form-login> n'est pas paramétré, Spring Security génère une page de formulaire par défaut :

SpringSecurity8.png

Page de login personnalisée

Evidemment, il est souhaitable de spécifier une page de login personnalisée à l'aide de l'attribut login-page :

<security:http>
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login login-page="/login.jsp"/>
</security:http>

A noter aussi l'ajout d'une règle d'interception avec filters="none" afin que la page de login ne soit pas filtrée par Spring Security. Sans cette règle, l'application tombe dans une boucle infinie qui tente d'afficher la page login.jsp, mais qui nécessiterait aussi d'être authentifiée (suivant la dernière règle avec le pattern /**).

Par ailleurs, la page de login doit respecter certaines règles :

<form method="post" action="j_spring_security_check">
  Identifiant  :<input name="j_username" value="" type="text" />
  Mot de passe :<input name="j_password" type="password" />
  <input value="Valider" type="submit" />
</form>

Nous disposons dorénavant de notre page personnalisée pour l'authentification :

SpringSecurity9.png

Page d'échec d'authentification

L'attribut authentication-failure-url permet de spécifier une page indiquant un échec de l'authentification (par défaut, on boucle sur la page de login). De même que pour la page de login, il faut penser à déclarer une règle d'interception afin de ne pas filtrer la page d'erreur.

<security:http>
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/login-failure.jsp" filters="none"/>
  <security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login login-page="/login.jsp"
                      authentication-failure-url="/login-failure.jsp"/>
</security:http>

Quand l'utilisateur saisit un identifiant ou un mot de passe incorrect, la page login-failure.jsp est alors affichée :

SpringSecurity10.png

URL post login

Quand une URL est sollicitée par l'utilisateur, la page de login est affichée suivant le rôle nécessaire à l'accès de la page. Une fois l'authentification effectuée, Spring Security renvoie la page initialement demandée :

SpringSecurity13.png

Si la première page demandée par l'utilisateur est la page de login elle-même, alors Spring Security redirige la requête vers la page du welcome file list après l'authentification. Il est cependant possible de redéfinir la page post login à l'aide de l'attribut default-target-url :

<security:form-login login-page="/login.jsp" 
                     default-target-url="/accueil.jsp"/>

Ce qui donne :

SpringSecurity14.png


Par ailleurs, si on souhaite forcer l'affichage de la default-target-url quelle que soit l'URL initialement sollicitée par l'utilisateur, il suffit de positionner l'attribut always-use-default-target à true :

<security:form-login login-page="/login.jsp" 
                     default-target-url="/accueil.jsp" 
                     always-use-default-target="true"/>


SpringSecurity15.png


Page indiquant un accès non-autorisé

Quand un utilisateur authentifié tente d'accéder à une ressource non-autorisée, Spring Security renvoie par défaut une erreur HTTP 403. A la place, on peut demander l'affichage d'une page d'erreur personnalisée à l'aide de l'attribut access-denied-page :

<security:http access-denied-page="/denied.jsp">
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login login-page="/login.jsp"/>
</security:http>
SpringSecurity17.png

Gestion du logout

Le logout est piloté par l'invocation de l'URL /j_spring_security_logout. Il provoque l'invalidation de la session HTTP et la redirection vers la page racine de l'application.

Pour l'activer, il suffit d'ajouter l'élément <logout> dans la configuration Spring :

<security:http>
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/editerproduit.do" access="ROLE_ADMIN"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login login-page="/login.jsp"/>
  <security:logout/>
</security:http>

Une JSP fournit le lien de déconnexion :

<a href="j_spring_security_logout">Déconnexion</a>

Il est possible de personnaliser le logout :

<security:logout logout-success-url="/login.jsp" 
                 logout-url="/doLogout" 
                 invalidate-session="false"/>


Authentification anonyme

L'authentification anonyme permet de créer un utilisateur sans identification explicite. Cette authentification automatique se produit au premier accès HTTP sur l'application. Les caractéristiques de cet utilisateur sont les suivantes :

On peut ainsi donner l'accès à certaines ressources à tout utilisateur non authentifié :

Pour activer ce mode d'authentification, on utilise l'élément <anonymous/> :

<security:http>
  <security:intercept-url pattern="/login.jsp"
                          access="ROLE_ANONYMOUS"/>
  <security:intercept-url pattern="/**/*.js"
                          access="ROLE_ANONYMOUS"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>
  <security:form-login login-page="/login.jsp"/>

  <security:anonymous />
</security:http>


Une fois l'utilisateur authentifié explicitement, anonymousUser est remplacé dans le security context par ce nouvel utilisateur. Il convient donc d'affecter le rôle anonyme à tous les utilisateurs pour ne pas filtrer l'accès aux ressources accessibles au rôle anonyme :

<security:user name="admin" password="admin"
               authorities="ROLE_ADMIN,ROLE_USER,ROLE_ANONYMOUS" /> 
<security:user name="guest" password="guest"
               authorities="ROLE_USER,ROLE_ANONYMOUS" />

Une autre solution consiste à ajouter les rôles nécessaires dans les règles d'interception des requêtes HTTP :

<security:intercept-url pattern="/login.jsp"
                        access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>
<security:intercept-url pattern="/**/*.js"
                        access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER"/>


Par ailleurs, on peut redéfinir l'identifiant et le rôle de l'utilisateur anonyme :

<security:anonymous username="anonyme"
                    granted-authority="ROLE_ANONYME"/>

Fonction remember-me

La fonction "remember-me" permet à un site de se rappeler de l'identité d'un principal entre deux sessions. Pour l'activer, il suffit d'ajouter l'élément <remember-me> dans le fichier de configuration Spring :

<security:http>
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login login-page="/login.jsp"/>

  <security:remember-me />
</security:http>

A noter que cette fonction nécessite une authentification par formulaire. Le formulaire doit par ailleurs contenir le paramètre _spring_security_remember_me :

<form method="post" action="j_spring_security_check">
  Identifiant :
  <input name="j_username" value="" type="text" />
  Mot de passe :
  <input name="j_password" type="password" />
  Remember me ? 
  <input type="checkbox" name="_spring_security_remember_me">
  <input value="Valider" type="submit" />
</form>
SpringSecurity16.png

Par défaut, le jeton "remember-me" est stocké dans une cookie et contient les informations suivantes :

Bien que l'identifiant et le mot de passe soient cryptés dans le cookie, cette solution présente une faille de sécurité car ces informations sont exposées sur le réseau. Une alternative consiste alors à stocker les informations du jeton "remember-me" dans une base de données. Il suffit pour cela de préciser une data source à l'aide de l'attribut data-source-ref (cet attribut référence un bean Spring de type DataSource) :

<security:http>
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/**" access="ROLE_USER"/>

  <security:form-login login-page="/login.jsp"/>

  <security:remember-me data-source-ref="dataSource"/>
</security:http>

Spring Security stocke alors le jeton dans la table persistent_logins dont voici le script de création :

create table persistent_logins 
             (username varchar(64) not null,
              series varchar(64) primary key,
              token varchar(64) not null, 
              last_used timestamp not null)

Configuration auto-config

L'attribut auto-config permet de simplifier la configuration de Spring Security au maximum. Elle inclut par défaut les fonctionnalités suivantes :

Il suffit alors de fournir une règle d'interception d'URL pour activer Spring Security :

<security:http auto-config="true">
  <security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>

Les éléments de configuration peuvent être redéfinis selon les besoins :

<security:http auto-config="true">
  <security:intercept-url pattern="/login.jsp" filters="none"/>
  <security:intercept-url pattern="/**" access="ROLE_USER" />
  <security:form-login login-page="/login.jsp"/>
</security:http>

Authentication provider

Crypter les mots de passe

Les mots de passe peuvent être cryptés à l'aide de l'élément password-encoder. Plusieurs algorithmes de cryptage sont disponibles en standards :

<security:authentication-provider>
  <security:password-encoder hash="sha"/>
  <security:user-service>
    <security:user name="admin"
                   password="d033e22ae348aeb5660fc2140aec35850c4da997"
                   authorities="ROLE_ADMIN,ROLE_USER" /> 
    <security:user name="guest"
                   password="35675e68f4b5af7b995d9205ad0fc43842f16450"
                   authorities="ROLE_USER,ROLE_ANONYMOUS" /> 
  </security:user-service>
</security:authentication-provider>


Afin de renforcer la sécurité, on peut préciser une valeur "salt" à l'algorithme de cryptage via l'élément salt-source.

La valeur "salt" peut être unique pour tous les utilisateurs (attribut system-wide) :

<security:password-encoder hash="sha">
  <security:salt-source system-wide="ma valeur"/>
</security:password-encoder>

La valeur "salt" peut aussi être générée pour chaque utilisateur (attribut user-property) à partir d'une propriété de l'objet UserDetails (cette stratégie renforce encore plus la sécurité) :

<security:password-encoder hash="sha">
  <security:salt-source user-property="username"/>
</security:password-encoder>


Note : UserDetails est une interface qui définit les informations d'identification pour chaque utilisateur :

SpringSecurity18.png


Enfin, Spring Security fournit des classes qui permettent de crypter les mots de passe. Voici un exemple pour l'algorithme sha :

ShaPasswordEncoder encoder = new ShaPasswordEncoder();
String cryptedPassword = encoder.encodePassword(password, saltValue));


Provider JDBC

Les informations d'authentification et d'habilitation peuvent être stockées en base de données :

<security:authentication-provider>
  <security:jdbc-user-service data-source-ref="dataSource" />
</security:authentication-provider>

L'attribut data-source-ref permettant de référencer un bean Spring de type DataSource.

Quand on déclare un service de type jdbc-user-service, Spring utilise une classe DAO qui respecte le schéma SQL suivant :

CREATE TABLE users (
  username VARCHAR(50) NOT NULL PRIMARY KEY,
  password VARCHAR(50) NOT NULL,
  enabled BIT NOT NULL
);
 
CREATE TABLE authorities (
  username VARCHAR(50) NOT NULL,
  authority VARCHAR(50) NOT NULL
);
 
ALTER TABLE authorities ADD CONSTRAINT fk_authorities_users foreign key (username) REFERENCES users(username);

Il est possible de personnaliser les requêtes SQL de ce service JDBC si les noms des tables et colonnes de la base de données "utilisateurs" sont différents du schéma ci-dessus :

<security:authentication-provider>
  <security:jdbc-user-service data-source-ref="dataSource" 
                              users-by-username-query="SELECT identifiant,motdepasse,actif FROM utilisateurs WHERE identifiant = ?"
                              authorities-by-username-query="SELECT identifiant,role FROM roles WHERE identifiant = ?"/>
</security:authentication-provider>


Si le schéma de la base de données "utilisateurs" ne permet pas d'appliquer ces requêtes, il est alors nécessaire de créer un service spécifique par implémentation de l'interface UserDetailsService.

Utiliser la taglib de Spring Security

L'objectif de cette taglib est de permettre :

Déclaration de la taglib dans une JSP :

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

Le tag "authentication"

Il permet d'afficher une propriété de l'objet Authentication (interface de Spring Security) :

Utilisateur : <sec:authentication property="principal.username"/>

Ainsi que le montre l'exemple ci-dessus, l'attribut property peut référencer une propriété composée. En l'occurence, on affiche l'identifiant de l'utilisateur :

SpringSecurity20.png


Le tag "authorize"

Il permet d'afficher / cacher des fragments de JSP en fonction des rôles de l'utilisateur connecté.

Attributs du tag :

Dans l'exemple ci-dessous, on affiche l'identifiant de l'utilisateur s'il possède le rôle ROLE_USER :

<sec:authorize ifAllGranted="ROLE_USER">
  Utilisateur : <sec:authentication property="principal.username"/>
</sec:authorize>