Spring Security

Cette page a été rédigée il y a fort fort longtemps, et n'a pas tellement été mise à jour.

 

Vous savez, moi je ne crois pas qu'il y ait de bonne ou de mauvaise page. Moi, si je devais résumer mon wiki aujourd'hui avec vous, je dirais que c'est d'abord des rencontres. Des gens qui m'ont tendu la main, peut-être à un moment où je ne pouvais pas, où j'étais seul chez moi. Et c'est assez curieux de se dire que les hasards, les rencontres forgent une destinée... Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas l'interlocuteur en face je dirais, le miroir qui vous aide à avancer. Alors ça n'est pas mon cas, comme je disais là, puisque moi au contraire, j'ai pu ; et je dis merci au wiki, je lui dis merci, je chante le wiki, je danse le wiki... je ne suis qu'amour ! Et finalement, quand des gens me disent « Mais comment fais-tu pour avoir cette humanité ? », je leur réponds très simplement que c'est ce goût de l'amour, ce goût donc qui m'a poussé aujourd'hui à entreprendre une construction logicielle... mais demain qui sait ? Peut-être simplement à me mettre au service de la communauté, à faire le don, le don de soi.

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 :

  • Intégration dans la session HTTP des informations de sécurité contenues dans la requête

  • Vérification de l’identité de l’appelant et affichage d’une invite de connexion si nécessaire

  • Vérification des droits d’accès à la ressource sollicitée *…​

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 :

  • Authentification anonyme

  • Fonction Remember Me

  • Gestion NTLM

  • Intégration avec un serveur LDAP ou un serveur CAS

  • Gestion des certificats X509

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 :

  • spring-security-core-2.X.X.jar

  • spring-security-taglibs-2.X.X.jar

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="link:http://www.springframework.org/schema/beans[http://www.springframework.org/schema/beans]"
       xmlns:xsi="link:http://www.w3.org/2001/XMLSchema-instance[http://www.w3.org/2001/XMLSchema-instance]"
       xmlns:security="link:http://www.springframework.org/schema/security[http://www.springframework.org/schema/security]"
       xsi:schemaLocation="link:http://www.springframework.org/schema/beans[http://www.springframework.org/schema/beans] link:http://www.springframework.org/schema/beans/spring-beans-2.0.xsd[http://www.springframework.org/schema/beans/spring-beans-2.0.xsd]
                           link:http://www.springframework.org/schema/security[http://www.springframework.org/schema/security] link:http://www.springframework.org/schema/security/spring-security-2.0.4.xsd[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 :

  • La base de données utilisateur est définie en dur dans le fichier de configuration. Pour chaque utilisateur, on spécifie l’identifiant, le mot de passe et les rôles qui lui sont attribués avec l’élément user.

  • On définit aussi les règles d’accès aux pages de l’application à l’aide de l’élément intercept-url. Il s’agit simplement d’associer un pattern d’URL à un ou plusieurs rôles (séparation avec une virgule). A noter que les patterns d’URL sont traités dans l’ordre de déclaration : si /** était déclaré en premier, les autres patterns ne seraient pas vérifiés.

  • Le dernier élément http-basic permet d’afficher une invite de connexion avec une popup pour demander l’identification de l’utilisateur :

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 :

  • action ⇒ j_spring_security_check

  • nom input identifiant ⇒ j_username

  • nom input mot de passe ⇒ j_password

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

  • Attribut logout-success-url : page affichée après le logout

  • Attribut logout-url : pour redéfinir j_spring_security_logout

  • Attribut invalidate-session : true par défaut

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

  • Identifiant : anonymousUser

  • Rôle : ROLE_ANONYMOUS

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

  • Page d’accueil

  • Page de login

  • Images

  • Fichiers CSS et JavaScript

  • …​

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 :

  • Identifiant

  • Mot de passe

  • Date d’expiration

  • Clé privé

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 :

  • Authentication par formulaire généré par Spring

  • Authentication anonyme activé

  • Fonctions logout et remember-me activées

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 :

  • plaintext

  • sha

  • md5

  • md4

  • …​

<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’afficher des informations concernant l’utilisateur connecté

  • d’afficher / cacher des fragments de JSP en fonction des rôles de l’utilisateur connecté

Déclaration de la taglib dans une JSP :

<%@ taglib prefix="sec" uri="link:http://www.springframework.org/security/tags[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 :

  • ifAllGranted : teste si l’utilisateur a tous les rôles indiqués

  • ifAnyGranted : teste si l’utilisateur a un des rôles indiqués

  • ifNotGranted : teste si l’utilisateur n’a aucun des rôles indiqués

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>