samedi 19 mars 2011

Sécuriser une appli GWT/GAE avec SpringSecurity – part 1

Sécuriser une appli GWT/GAE avec SpringSecurity – part 1

Le but de cet article est de décrire comment mettre en place une solution "simple" d'authentification et de sécurisation d'une appli web basée sur Google Web Toolit (GWT) et Google App Engine (GAE) en utilisant le framework Spring Security.

Ne connaissant pas du tout Spring Security à la base (et très peu Spring), j'ai du faire pas mal de recherche sur le net pour mettre en place mon appli.
On peut trouver beaucoup d'articles parlant de GAE + Spring Security, ou GWT + Spring Security, mais assez peu regroupant les 3 technos ensembles, c'est donc pour ça que je fais cet article.

La solution proposée est surement loin d'être parfaite, mais elle fonctionne, je ne rentrerai pas non plus dans les entrailles de Spring, puisque que je ne connais pas bien ce domaine (et ce n'est pas le sujet de cet article), toutefois, si vous avez des idées pour améliorer le système ou si vous voyez des incohérences / erreurs, n'hésitez pas à m'en faire part.

Pour mettre en place ma solution, je me suis basé sur différents articles qui comme dis plus haut traitent une partie du problème, voici les principaux :

Technos utilisées :

Contexte :

  • Une appli 'simple' GWT faisant des appels RPC vers GAE doit être sécurisée.
  • Les utilisateurs doivent avoir un compte propre à l'appli pour se connecter (pas de compte Google, d'OpenID ou autre).
  • Toute l'application est accessible uniquement par des utilisateurs authentifiés, il n'y à pas de partie "publique" (a part la partie Login bien sur).

Création du projet :

J'ai générer sous Eclipse un nouveau projet GWT, en utilisant GAE, et laissant Eclipse générer du "contenu", ça permet d'avoir en quelques secondes une appli simple et prête à être sécurisée.

Je laisse le code généré comme il est pour le moment, on va essayer d’être le moins invasif possible pour pouvoir sécuriser n’importe quelle appli sans avoir à réécrire 200 classes.

Configuration de l'appli :

La première chose à faire, est de configurer SpringSecurity (en fait la première chose à faire est de définir les librairies Spring dans le classpath du projet et dans le folder WEB-INF/lib).

Une fois le classpath définis, on va dans le fichier web.xml, il faut ajouter ces quelques lignes qui permettent de définir le(s) fichier(s) de config de Spring et de déléguer la gestion des filtres à ce dernier :

contextConfigLocation – fichier de config de SpringSecurity
ContextLoaderListener
DelegatingFilterProxy – délègue la gestion des filtres à Spring

web.xml

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml </param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Spring Security Filter -->
<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>

Il faut aussi penser à activer les sessions dans GAE, pour ça il suffit d'ajouter 1 ligne dans appengine.xml :

appengine.xml

<sessions-enabled>true</sessions-enabled>

Pour que les utilisateurs puissent s'authentifier, j'ai choisit de passer par un simple formulaire HTML, avec username/password, en reprenant les noms de champs utilisés par Spring.

Je crée une page Login.jsp (je fais une JSP et non un page HTML car dans GAE, les pages HTML sont considérés comme des ressources statiques, et Spring ne peut pas y accéder. Voici un article qui détails un peu plus ce point)

Login.jsp

<form name='f' action='/j_spring_security_check' method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='j_username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='j_password' /></td>
</tr>
<tr>
<td colspan='2'><input name="submit" type="submit" /></td>
</tr>
</table>
</form>

Configurer Spring :
On commence par définir les namespaces de Spring (cf. SpringNameSpaces pour plus de détails).
On autorise ensuite les annotations pour simplifier la sécurité sur les appels RPC en utilisant des annotations (comme @Secure(«role») ).

applicationContext.xml

<beans:beans
xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
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.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">


<global-method-security secured-annotations="enabled" jsr250-annotations="disabled" />

On va pouvoir maintenant définir la partie Sécurité à proprement parlé :

applicationContext.xml

<http auto-config=”true”  use-expressions="true">
<intercept-url pattern="/Login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER" />
</http>

Ce bout de code, permet de filtrer les requêtes et de définir un niveau d'autorisation pour chacune d'entres elles. En mettant auto-config=true, on laisse Spring faire la config de base, et on va juste adapter ce dont on a besoin.

On définit 2 types de requêtes :

  • celles vers la page de Login, qui doit être accessible par tout le monde (IS_AUTHENTICATED_ANONYMOUSLY)
  • et celles vers le reste de l’appli ( /** ) qui sont limitées au utilisateurs ayant le rôle "ROLE_USER".

On définit aussi la page de login personnalisée et la page vers laquelle les utilisateurs seront redirigés si l'authentification se passe bien :

applicationContext.xml

<http auto-config=”true” use-expressions="true">
<intercept-url pattern="/Login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login
login-page="/Login.jsp"
default-target-url="/SimpleSecurityExample.jsp"/>
</http>

A ce niveau, nous avons donc définis des filtres forçant les utilisateurs à s'authentifier pour accéder à l'appli, sauf pour la page Login.jsp qui est en accès libre. Il nous reste à définir la manière d'authentifier les utilisateurs.

Pour ça, on va utiliser l’authentification-manager et les authentication-provider de Spring.

Le premier sera appelé directement par un des filtres de Spring, et le 2e permet de définir la méthode d’authentification. On peut combiner plusieurs authentication-provider si l’appli doit authentifier les utilisateurs auprès de plusieurs sources (DB locale, OpenID, LDAP, etc.)

La doc officielle de Spring détails assez bien la gestion de ces 2 éléments si vous voulez plus de détails : DOC

Pour du testing, on peut utiliser un authentication-provider en « dur » qui permet de définir des utilisateurs directement dans applicationContext.xml, ca permet de voir si la partie déjà écrite fonctionne ou non, voila un exemple d'utilisateurs avec différents rôles :

applicationContext.xml

<authentication-manager  alias="authenticationManager">
<authentication-provider>
<user-service>
<user username=”toto” password=”toto” authority=”ROLE_USER”/>
<user username=”root” password=”root” authority=”ROLE_ADMIN”/>
</user-service>
</authentication-provider>
</authentication-manager>

On peut compiler l’application et vérifier normalement tout fonctionne, si j’essai d’accéder à la page http://127.0.0.1:8888/SimpleSecurityExample.jsp je suis redirigée automatiquement vers l’adresse http://127.0.0.1:8888/Login.jsp.

Si j’utilise les accès toto / toto, j’accède à mon appli sans problème.

Maintenant que nous pouvons nous connecter avec une liste d’utilisateurs prédéfinis, il est temps de gérer nos propres utilisateurs avec GAE et son datastore.

On ajoute donc un nouvel authentification- provider de type DaoAuthenticationProvider, en utilisant ce Bean, nous évitons d’avoir à écrire notre propre provider, il suffit de paramétrer celui par défaut. Voila comment le définir :

applicationContext.xml

<authentication-manager  alias="authenticationManager">
<authentication-provider>
<user-service>
<user username=”toto” password=”toto” authority=”ROLE_USER”/>
<user username=”root” password=”root” authority=”ROLE_ADMIN”/>
</user-service>
</authentication-provider>
<authentication-provider ref="simpleAuthenticationProvider"/>
</authentication-manager>



<!-- Custom Authentication Provider -->
<bean id="simpleAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
</bean>

Pour que le provider puisse accéder aux infos du DataStore, il va passer par un service appelé UserDetailsService, qui ne contient qu’une méthode qui à partir d’un nom d’utilisateur va aller retrouver les infos (nom d’utilisateur, mot de passe et autorisations) dans la source de données (ici le Datastore GAE). Ce service retourne un objet de type UserDetail, qui regroupe ces différentes infos. Le provider va utiliser l’objet retourné et les infos récupérées dans le formulaire pour vérifier si l’utilisateur est valide ou non.

Il nous suffit donc de définir notre propre implémentation de UserDetailsService et de la passer au provider définis plus haut.

Voila la déclaration du bean :

applicationContext.xml

<!-- Custom  UserDetail Service -->
<bean id="simpleUserDetailsService" class="com.example.SimpleUserDetailsService"/>

Et comment l’injecter dans notre authentication-provider :

applicationContext.xml

<!-- Custom  Authentication Provider -->
<bean id="simpleAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="simpleUserDetailsService"/>
</bean>


A ce stade, la config de Spring est terminée, et notre fichier applicationContext.xml ressemble donc à ca :

applicationContext.xml

<http auto-config=”true” use-expressions="true">
<intercept-url pattern="/Login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login
login-page="/Login.jsp"
default-target-url="/SimpleSecurityExample.jsp"/>
</http>


<!-- Authentication Manager -->
<authentication-manager alias="authenticationManager">
<authentication-provider>
<user-service>
<user username=”toto” password=”toto” authority=”ROLE_USER”/>
<user username=”root” password=”root” authority=”ROLE_ADMIN”/>
</user-service>
</authentication-provider>
<authentication-provider ref="simpleAuthenticationProvider"/>
</authentication-manager>
<!-- Custom Authentication Provider -->
<bean id="simpleAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="simpleUserDetailsService"/>
</bean>

<!-- Custom UserDetail Service -->
<bean id="simpleUserDetailsService" class="com.example.SimpleUserDetailsService"/>

Il nous fait maintenant créer la classe qui implémente le UserDetailsService que l’on vient de définir.

C’est une implémentation simple de l’interface qui doit définir la méthode loadUserByUsername(String username) et retourner un objet de type UserDetail.

SimpleUserDetailsService.java

public class SimpleUserDetailsService impements UserDetailsService {

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO
};

Il y à différentes méthodes pour récupérer les infos dans le datastore, on peut le faire manuellement (comme ici) ou passer par un autre framework ou une autre librairie (Objectify par exemple).

SimpleUserDetailsService.java

public class SimpleUserDetailsService impements UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

Key key = KeyFactory.createKey(“SimpleUser”, username);
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

try {
Entity user = datastore.get(key);

GrantedAuthority[] role = new GrantedAuthority[]{new GrantedAuthorityImpl("ROLE_USER")};
String usernameValue = (String)user.getProperty(“username”);
String password = (String)user.getProperty(“password”);
boolean active = (Boolean)user.getProperty(“status”);
User usr = new User(usernameValue,password ,active ,true,true,true,role);

return usr;

} catch (EntityNotFoundException e) {
return null;
}
};

Il nous reste plus qu’à sécuriser le GreetingService pour être complet (nous avons sécurisé la page qui accède à ce service, mais pas le service lui-même pour le moment).

Comme nous avons activé la gestion des annotations dans la config de Spring, il suffit de modifier la déclaration de notre méthode dans l’interface de notre service :

GreetingService.java

@Secured("ROLE_USER")
String greetServer(String name);

Il ne faut pas oublier d’ajouter la nouvelle exception qui peut être déclenchée par Spring.

La partie code est maintenant terminée, si je recompile mon appli, je peux maintenant me connecter en utilisant soit l’utilisateur définis en dur (toto) comme tout à l’heure, soit un utilisateur de mon datastore (à condition d’avoir chargé celui-ci avant …)

On peut voir dans les logs que selon le username utilisé, Spring passe (ou non) par notre UserDetailsService :

La sécurisation ‘simple’ est donc finie, je ferai un autre article pour essayer d’améliorer un peu le process avec notamment :

    • gestion du logout
    • gestion des timeout pour la session
    • gestion du Remember-me
    • personnalisation des erreurs/exceptions
    • gestion de plusieurs niveaux d’accès (utilisateur simple / administrateur)
    • encodage des passwords
    • gestion des données utilisateurs dans les services
    • etc.

Les sources liées à cet article sont dispo (ou le seront bientôt) à cette adresse :

http://code.google.com/p/eryos-example/

Si vous avez des questions ou remarques, n’hésitez pas ;-)