Spring Security

Introduction

Spring est un framework open source JEE pour les applications n-tiers, dont il facilite le développement et les tests. Il est considéré comme un conteneur dit « léger », c’est-à-dire une infrastructure similaire à un serveur d’application J2EE. Il prend donc en charge la création d’objets et la mise en relation d’objets par l’intermédiaire d’un fichier de configuration qui décrit les objets à fabriquer et les relations de dépendances entre ces objets.

Le gros avantage par rapport aux serveurs d’application est qu’avec Spring, vos classes n’ont pas besoin d’implémenter une quelconque interface pour être prises en charge par le framework (au contraire des serveurs d’applications J2EE et des EJBs). C’est en ce sens que Spring est qualifié de conteneur « léger ».
Spring s’appuie principalement sur l’intégration de trois concepts clés :

  1. l’inversion de contrôle ou injection de dépendance (IoC).
  2. la programmation orientée aspect (AOP).
  3. une couche d’abstraction.

Ce framework, grâce à sa couche d’abstraction, ne concurrence pas d’autres frameworks dans une couche spécifique d’un modèle architectural MVC mais s’avère un framework multi-couches pouvant s’insérer au niveau de toutes les couches.

Nous allons aborder dans ce tutoriel le module Security de Spring anciennement appelé Acegi Security System.

Pourquoi utiliser Spring Security? Spring Security fournit une solution complète en matière de sécurité pour les applications Java JEE. Ce module permet de configurer toute la sécurité sur l’ensemble d’un système sans être dépendant de l’environnement.

Site Officiel

Vous trouverez le site officiel de Spring à l’adresse suivante : http://static.springsource.org/spring-security/site/

Wikipédia

Je vous invite à consulter les articles français et anglais de la célèbre encyclopédie Wikipédia à propos du Framework Spring.

Tutoriel

Avant d’entamer ce tutoriel, vérifiez que vous avez bien les pré requis :

  • Le JDK doit être installé sur votre machine.
  • Eclipse doit être installé sur votre machine (il est conseillé d’utiliser la version Eclipse IDE for Java EE Developers pour ce tutoriel).
  • Avez vous réalisé le tutoriel sur Spring IOC?
  • Avez vous réalisé le tutoriel sur Spring AOP?

Nous allons commencer par détailler les principales notions en matière de sécurité concernant une application Web pour ensuite montrer comment on peut gérer l’accès à des services avec le module Security de Spring.

Télécharger Spring Security

On commence par télécharger Spring Security sur le site officiel à l’adresse suivante.

Quelques concepts clés en matière de sécurité

Avant de se lancer dans notre exemple pour le tutoriel, il faut tout d’abord comprendre quelques notions générales sur la sécurité. Nous allons les aborder dans cette partie et expliquer et faire un parallèle rapide avec Spring Security.

Authentification

L’authentification est un concept de base pour la sécurité de manière générale, qui permet de vérifier l’identité d’une entité (en général un utilisateur), pour l’autoriser d’accéder à des ressources (systèmes, réseaux, applications…). L’authentification permet donc de valider l’authenticité de l’entité en question. La méthode la plus classique d’authentification est l’utilisation d’un login/password (ou identifiant/mot de passe).

Autorisations

Au delà de l’authentification, on a ensuite le concept d’autorisation. Cela permet ensuite de gérer de manière atomique les autorisations au sein d’une ressource. Par exemple dans une application, une fois qu’un utilisateur est authentifié, on peut ensuite lui autoriser l’accès à certains services seulement suivant son rôle, ses droits, etc…
Spring Security permet de protéger n’importe quelle ressource, qu’il s’agisse d’une url, d’une méthode, ou d’un objet.

Comment gérer l’Authentification

On commence par créer un nouveau projet dans Eclipse :

  1. Lancer Eclipse
  2. Menu File → New → Java project
  3. Dans la fenêtre New Java Project, on indique le nom : sf-tutorial-spring-security
  4. On valide en cliquant sur Finish.
  5. On ajoute au buildpath les librairies suivantes :
  • spring-security-core-3.1.1.RELEASE.jar
  • commons-logging-1.1.1.jar
  • log4j-1.2.16.jar
  • spring.jar
  • spring-aop-3.1.1.RELEASE.jar
  • aspectjrt-1.5.3.jar
  • junit-4.4.jar (normalement Eclipse ajoute lui même cette librairie à la création d’un test unitaire en utilisant l’assistant de création.)
  • spring-security-config-3.1.1.RELEASE.jar
  • spring-core-3.1.2.RELEASE.jar
  • aspectjrt-1.5.3.jar
  • spring-beans-3.1.1.RELEASE.jar
  • spring-asm-3.1.1.RELEASE.jar
  • aopalliance-1.0.jar
  • spring-context-3.1.1.RELEASE.jar

Spring Security gère un large panel de modèles d’authentification. Pour se concentrer uniquement sur la sécurité dans le tutoriel, nous n’allons pas mettre en place une interface graphique Web pour s’authentifier mais le faire directement dans nos tests unitaires. Spring Security offre cette possibilité, pour cela, il suffit de l’indiquer dans le fichier XML de configuration Spring. On crée donc un fichier spring.security.context.xml qui contient le code suivant :

<?xml version="1.0" encoding="UTF-8"?>
 
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:sec="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
 
	<!-- Authentification -->
	<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
		<property name="providers">
			<list>
				<ref local="daoAuthenticationProvider" />
			</list>
		</property>
	</bean>
 
	<bean id="daoAuthenticationProvider"
		class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
		<property name="userDetailsService" ref="myUsersService" />
	</bean>
 
	<sec:user-service id="myUsersService">
		<sec:user name="straumat" password="straumat16"
			authorities="ROLE_USER, ROLE_ADMIN" />
		<sec:user name="scubfoundation" password="scubfoundation16"
			authorities="ROLE_USER" />
	</sec:user-service>
        ...

Détaillons un peu le rôle de chacun des bean déclarés ci dessus :

  • ProviderManager : gère les requêtes d’authentification provenant d’autres modules du Framework Spring.
  • DaoAuthenticationProvider : définit les mécanismes sur lesquels s’appuie le manager d’authentification pour authentifier les utilisateurs.
  • UserDetailsService (ici exprimé sous la forme de user-service) : ici il contient la déclaration des différents utilisateurs de l’application.


Comment sécuriser des services

Nous allons maintenant montrer comment sécuriser l’accès aux services d’une application en utilisant Spring Security. Cet exemple est volontairement simplifié afin de se concentrer sur les mécanismes du module Security de Spring.

Implémentation des services

On définit tout d’abord une interface qui décrit nos services :

  1. Clic droit sur répertoire src du nouveau projet dans le Package Explorer.
  2. Dans le menu contextuel on va dans New → interface
  3. Dans fenêtre New Java interface, on indique :
    • le nom du package suivant : com.scub.foundation.tutorial.spring.security.services
    • le nom de la classe : ServicesExemple
  4. On valide en cliquant sur Finish.

Voici le code ci dessous de notre interface ServicesExemple qui contient les signatures pour 3 services simples qui serviront dans notre exemple :

  1. afficherMsgBienvenue() : service qui affiche un message de bienvenue, accessible à tous les utilisateurs .
  2. afficherDateDuJour() : service qui affiche la date du jour, accessible à tous les utilisateurs .
  3. afficherDonneesSensibles() : service qui affiche des données sensibles, accessible uniquement à des utilisateurs qualifiés.
package com.scub.foundation.tutorial.spring.security.services;
 
/**
 * Interface des Services.
 * @author Scub-Foundation
 */
public interface ServicesExemple {
 
	/** Affiche la date du jour. */
	public void afficherDateDuJour();
 
	/** Service qui affiche un message de bienvenue. */
	public void afficherMsgBienvenue();
 
	/** Affiche des données. */
	public void afficherDonneesSensibles();
 
}

On implémente ensuite notre interface dans une classe qui va contenir nos services :

  1. Clic droit sur répertoire src du nouveau projet dans le Package Explorer.
  2. Dans le menu contextuel on va dans New → class
  3. Dans fenêtre New Java class, on indique :
  • le nom du package suivant : com.scub.foundation.tutorial.spring.security.services
  • le nom de la classe : ServicesExempleImpl
  • interface implémentée : ServicesExemple
  • On valide en cliquant sur Finish.

Voici le code source de notre classe :

package com.scub.foundation.tutorial.spring.security.services;
 
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
 
/**
 * Classe implémentant des services exemples.
 * @author Scub-Foundation
 */
public class ServicesExempleImpl implements ServicesExemple {
 
	/**
	 * {@inheritDoc}
	 */
	public void afficherDateDuJour(){
		DateFormat format = new SimpleDateFormat("dd/MM/yyyy");
		System.out.println("Date du jour : " + format.format(new Date()));
	}
 
	/**
	 * {@inheritDoc}
	 */
	public void afficherMsgBienvenue(){
		System.out.println("Bienvenue dans le tutoriel de Scub Foundation sur Spring Security!");
	}
 
	/**
	 * {@inheritDoc}
	 */
	public void afficherDonneesSensibles(){
		System.out.println("Vous accedez ici à des données confidentielles!");
	}
 
}


Sécurisation des services

Maintenant que nous avons implémenté nos trois services, nous allons voir comment on peut les sécuriser. Pour rester simple et efficace, on sécurisera nos services de la manière suivante :

  1. afficherDateDuJour() : Ce service sera publique, tout le monde peut y accéder, y compris une personne non authentifiée.
  2. afficherMsgBienvenue() : Ce service n’est accessible qu’aux utilisateurs authentifiés.
  3. afficherDonneesSensibles() : Ce service n’est accessible qu’aux utilisateurs ayant les droits administrateur.

Pour sécuriser nos méthodes de services, il suffit d’ajouter au fichier de configuration de Spring les lignes suivantes :

        ...
	<!-- Sécurisation des Méthodes-->
	<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased" >
		<property name="decisionVoters">
			<list>
				<ref bean="roleVoter" />    <!-- le vote se fait via le rôle de l'utilisateur -->
			</list>
		</property>
	</bean>
 
	<bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter" />
 
	<sec:method-security-metadata-source id="securityMetadataSource">
		<sec:protect method="com.scub.foundation.tutorial.spring.security.services.ServicesExempleImpl.afficherMsgBienvenue*" access="ROLE_USER,ROLE_ADMIN"/>
		<sec:protect method="com.scub.foundation.tutorial.spring.security.services.ServicesExempleImpl.afficherDonneesSensibles*" access="ROLE_ADMIN"/>
	</sec:method-security-metadata-source>
 
	<bean id="ServicesExemplesManagerSecurity"
		class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
		<property name="authenticationManager" ref="authenticationManager" />
		<property name="accessDecisionManager" ref="accessDecisionManager" />
		<property name="securityMetadataSource" ref="securityMetadataSource" />
	</bean>
 
	<!-- Déclaration des Services -->
	<bean id="ServicesExempleTarget"
		class="com.scub.foundation.tutorial.spring.security.services.ServicesExempleImpl" />
	<bean id="ServicesExemple"
class="org.springframework.aop.framework.ProxyFactoryBean" />
		<property name="proxyInterfaces" value="com.scub.foundation.tutorial.spring.security.services.ServicesExemple"/>
		<property name="interceptorNames">
			<list>
				<idref local="ServicesExemplesManagerSecurity" />
				<idref local="ServicesExempleTarget" />
			</list>
		</property>
	</bean>
</beans>

Test Unitaire

Nous allons maintenant tester l’accès aux services en s’authentifiant. Pour vérifier que :

  • Le système d’authentification marche correctement
  • L’accès à chaque service en fonction de l’utilisateur authentifié est bien respecté

Voici le code ci dessous de notre classe de test unitaire :

package com.scub.foundation.tutorial.spring.security.test;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
 
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
 
import com.scub.foundation.tutorial.spring.security.services.ServicesExemple;
 
/**
 * Test unitaire d'utilisation des services en fonction des utilisateurs.
 * @author Scub-Foundation
 */
public class TestServices {
 
	/** Usine de fabricaton des beans. */
	private ListableBeanFactory bf;
 
	/** Classe contenant les exemples de services pour le tutoriel. */
	private ServicesExemple service;
 
    /**
     * Création du contexte sécurisé.
     * @param username le nom d'utilisateur
     * @param password le mot de passe
     */
    protected final void createSecureContext(final String username, final String password) {
        destroySecurityContext();
        final ProviderManager providerManager = (ProviderManager) bf.getBean("authenticationManager");
        final Authentication auth = providerManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
 
    /** Détruit le contexte de sécurité. */
    protected final void destroySecurityContext() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }
 
	/**
	 * Méthode appelée avant chaque méthode de test
	 * 
	 * @throws Exception
	 */
	@Before
	public void setUp() throws Exception {
		bf = new XmlBeanFactory(new ClassPathResource("spring.security.context.xml"));
		service = (ServicesExemple) bf.getBean("ServicesExemple");
	}
 
	/**
	 * Méthode de test du service publique afficherDateDuJour().
	 */
	@Test
	public void testServiceAfficherDateDuJour() {
 
		// On vérifie qu'aucun utilisateur est authentifié
		assertNull("Un utilisateur est authentifié",SecurityContextHolder.getContext().getAuthentication());
 
		System.out.println("Aucun utilisateur authentifié");
 
		service.afficherDateDuJour();		
	}
 
	/**
	 * Méthode de test du service afficherMsgBienvenue().
	 */
	@Test
	public void testServiceAfficherMsgBienvenue() {
		// Authentification en tant que simple utilisateur
		String login = "scubfoundation";
		String password = "scubfoundation16";
		createSecureContext(login, password);
 
		// On récupère le nom de l'utilisateur authentifié
		String utilisateurAuthentifie = SecurityContextHolder.getContext().getAuthentication().getName();
 
		// On vérifie que c'est bien le bon utilisateur qui a été authentifié
		assertEquals("l'utilisateur authentifié n'est pas le bon", login, utilisateurAuthentifie);
 
		// On affiche le nom de l'utilisateur qui vient de s'authentifier
		System.out.println("Utilisateur Authentifié : "+ utilisateurAuthentifie);
 
		// Appel aux services autorisés 
		service.afficherDateDuJour();
 
		service.afficherMsgBienvenue();
 
		// Désauthentification
		destroySecurityContext();
	}
 
	/**
	 * Méthode de test du service afficherDonneesSensibles().
	 */
	@Test
	public void testServiceAfficherDonneesSensibles() {
		// Authentification en tant qu'administrateur
		String login = "straumat";
		String password = "straumat16";
		createSecureContext(login, password);
 
		// On récupère le nom de l'utilisateur authentifié
		String utilisateurAuthentifie = SecurityContextHolder.getContext().getAuthentication().getName();
 
		// On vérifie que c'est bien le bon utilisateur qui a été authentifié
		assertEquals("l'utilisateur authentifié n'est pas le bon", login, utilisateurAuthentifie);
 
		// On affiche le nom de l'utilisateur qui vient de s'authentifier
		System.out.println("Utilisateur Authentifié : "+ utilisateurAuthentifie);
 
		// Appel aux services autorisés
		service.afficherDateDuJour();
 
		service.afficherMsgBienvenue();
 
		service.afficherDonneesSensibles();
 
		// Désauthentification
		destroySecurityContext();
	}
 
	/**
	 * Méthode de test d'appel à des services non autorisés en tant qu'utilisateur anonyme. 
	 */
	@Test
	public void testServiceNonAutorises() {
		// On vérifie qu'aucun utilisateur est authentifié
		assertNull("Un utilisateur est authentifié",SecurityContextHolder.getContext().getAuthentication());
 
			try {
				service.afficherMsgBienvenue();
			} catch (AuthenticationCredentialsNotFoundException e) {
				String msgException = "Aucun objet Authentication n'a été trouvé dans le SecurityContext";
				assertEquals("L'exception attendue n'est pas la bonne ",msgException,e.getMessage());
			}			
	}
}

A l’exécution, on obtient normalement 4 tests unitaires valides avec le résultat suivant dans la console :


Télécharger les sources

Les sources du projet Eclipse pour ce tutoriel sont téléchargeables sur Source Forge à l’adresse suivante.