Tutoriel sur la mise en place de Spring Security avec GWT

| David Martin | Ippon Technologies |

Beaucoup d'applications nécessitent la mise en œuvre de règles de sécurité dans tout ou partie de leurs fonctionnalités. Lorsque GWT est utilisé comme framework de présentation, les choses ne sont pas aussi simples qu'avec un framework plus traditionnel, où la nature des échanges client-serveur est plus claire et contrôlée.

Avant toute chose, commençons par définir ce qui sera présenté sous la notion de sécurité. L'objet du billet est de proposer à une application GWT la possibilité de gérer l'authentification des utilisateurs et le contrôle des droits sur les fonctionnalités exposées par le serveur. Ne sont donc pas présentés les aspects relatifs à la protection contre des attaques (XSS, CSRF, injections…).

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel sur la mise en place de Spring Security avec GWT

- Auteurs : David Martin, Ippon Technologies

- Parution : 11 novembre 2013

- Mise à jour : 15 février 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 David Martin. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Particularités de GWT

- II. Mécanismes de Spring Security

- III. La toolbox

- IV. L'authentification

- V. Contrôle des droits d'accès

- VI. Autres fonctionnalités

- VII. Assemblage du tout

- VIII. Conclusion

- IX. Remerciements

I - Particularités de GWT

Les mécanismes de communication client-serveur proposés par GWT sont multiples. Historiquement, GWT propose une approche bas niveau : le RequestBuilder - ainsi qu'une approche plus haut niveau, orientée RPC, et justement nommée GWT-RPC. Plus récemment, soit depuis GWT 2.1, un nouveau protocole d'échange, toujours basé sur http, est apparu, souvent désigné par son interface phare : RequestFactory. C'est un protocole orienté données, mais toujours de type RPC.

Alors que RequestBuilder permet d'avoir accès aux caractéristiques des requêtes et réponses http (ajout de headers http, choix du verbe http à utiliser…), les deux autres approches que sont GWT-RPC et RequestFactory se positionnent à un niveau d'abstraction plus élevé et masquent ou bloquent l'usage de toutes ces possibilités.

RequestBuilder reste une approche assez technique, souvent réservée à des besoins ponctuels précis, mais rarement répandue comme mécanisme global de communication avec les services exposés par le serveur. GWT-RPC et RequestFactory apportent une bien meilleure productivité (non sans autres coûts cependant…).

La partie sécurité illustrée dans ce billet va s'appuyer sur Spring Security pour implémenter les règles de sécurité. Le framework va apporter les facilités de gestion des droits, mode de connexion, déconnexion… Les principes présentés resteraient les mêmes pour un autre framework de sécurité (Apache Shiro (http://shiro.apache.org/) par exemple), mais nécessiteraient bien sûr une adaptation du code.

Voici les fonctions importantes qu'il convient de traiter dans le cadre de la sécurisation d'une application.

- Authentification : quel type et à quel niveau ?

- Validation des droits de l'utilisateur : vérification des droits et prise en charge des refus d'accès.

- Mécanisme de déconnexion, remember-me…

Avant d'aborder ces points, il est important de se pencher sur le cœur du problème : qu'est-ce qui rend une application GWT différente d'une application plus classique dans la gestion de la sécurité ?

La première différence notable est la sensibilité aux données retournées lors des appels aux services, principalement dans les cas d'utilisation reposant sur GWT-RPC ou RequestFactory. En effet, ces protocoles ne vont pas savoir interpréter les réponses classiques de Spring Security dans le cas d'une authentification nécessaire (traité classiquement par une redirection vers une page HTML contenant le formulaire de connexion) ou d'un refus d'accès à une ressource (redirection vers une page d'erreur 403 par exemple). Afin que ces protocoles soient éventuellement en mesure d'interpréter des messages correspondant à ces cas de figure, il leur faudra recevoir une réponse adaptée à leur format d'échange.

Il va donc être nécessaire au serveur de différencier une requête issue d'un composant GWT de toute autre requête, afin d'adapter sa réponse au besoin spécifique.

Un autre point important, qui concerne autant GWT-RPC que RequestFactory est la nature « RPC » de ces protocoles. Les URL qui seront appelées ne pourront pas (sauf effort supplémentaire coûteux et peu élégant) laisser présumer de l'action souhaitée côté serveur. Cette information appartient au « payload » de la requête. Et à moins de le décrypter (toujours possible, mais un peu coûteux) afin d'aller chercher cette information, il n'est pas possible d'appliquer une politique de sécurité basée uniquement sur un simple filtrage d'URL.

II - Mécanismes de Spring Security

Pour comprendre la proposition d'implémentation ci-après, il convient d'avoir à l'esprit quelques grands principes de fonctionnement de Spring Security.

Le framework Spring Security fonctionne par application d'un filtre (au sens Filter de la spécification Java Servlet) sur les requêtes reçues par l'application. Ce filtre gère en fait une chaîne ordonnée de sous filtres (au sens Spring Security cette fois), et agit donc comme une sorte de point d'entrée vers un ensemble de contrôles séquentiels appliqués aux requêtes. Chacun de ces contrôles est dédié à une vérification ou action particulière.

Autre point important concernant Spring Security : les différentes façons de définir et contrôler les droits. Spring Security propose une première approche basée sur des règles de filtre d'URL : il est possible d'associer à des patterns d'URL des privilèges requis pour y accéder (ex. : /admin/** -> ROLE_ADMIN).

Par ailleurs, en substitution ou en complément selon les cas, il est aussi possible d'utiliser une autre approche, par instrumentation des classes de l'application afin de leur appliquer des restrictions d'usage. Par configuration XML ou par annotation (beaucoup plus simple), un service pourra alors voir ses méthodes décorées de notions de droits d'accès (ex. : @Secured("ROLE_ADMIN") sur une méthode public boolean delete(MyResource resource));

Ces deux notions (fonctionnement par application d'une chaîne de filtres sur les requêtes ainsi que les deux modes de déclaration des contraintes de sécurité par patterns sur des URL d'une part ou instrumentation d'autre part) vont vous permettre de mieux comprendre ce qui suit.

Commençons maintenant le tour d'horizon des cas d'utilisation relatifs à la sécurité.

III - La toolbox

Afin de simplifier la création de la couche de sécurité, l'approche décrite ci-après s'appuie sur une petite bibliothèque et une application sample, que vous trouverez ici : https://github.com/dmartinpro/gwt-security/

IV - L'authentification

Comme vous le savez sûrement, l'authentification peut être réalisée de plusieurs façons. La plus courante consiste à utiliser un formulaire HTML afin de collecter dans la plupart des cas un identifiant accompagné d'un mot de passe. Le tout, en général, transite via une connexion sécurisée vers le serveur (SSL). Une autre approche, plus souvent utilisée pour sécuriser des services « techniques » (web service, API http), utilise l'authentification de type BASIC, toujours de façon sécurisée (clarté des informations oblige avec BASIC…). Il existe par ailleurs d'autres types (DIGEST, par certificat, indirect par SSO, custom).

Le mode d'authentification le plus ergonomique pour un utilisateur est celui par formulaire. Il permet d'offrir une présentation personnalisée, intégrée graphiquement à l'application… des qualités qu'une popup pour une authentification BASIC ne propose pas.

Dans le cas d'une application dont la couche de présentation est écrite en GWT, deux choix s'offrent alors : le formulaire d'authentification est-il aussi écrit avec GWT ou bien est-il proposé séparément sur une page dédiée ?

Illustrons le cas le plus intéressant : le formulaire est codé avec GWT (et donc mieux intégré au sein de l'application). Ce formulaire va être accédé de deux façons :

- volontaire : l'utilisateur affiche le formulaire pour y saisir les informations ;

- involontaire : l'utilisateur tente d'accéder à une ressource protégée, mais n'est pas encore connecté : il est redirigé vers le formulaire.

Dans les deux cas, il devra poster les informations saisies vers un service particulier (un filtre au sens Spring Security : UsernamePasswordAuthenticationFilter). Le code GWT lié au formulaire pourra s'appuyer sur RequestBuilder (voir com.gwtsecurity.client.LoginHandler) pour cela.

Le filtre UsernamePasswordAuthenticationFilter délègue à deux objets le soin de gérer les cas d'authentification réussie et en échec. Il est important d'ores et déjà de savoir répondre au composant GWT sous une forme intelligible spécifique, distincte du cas nominal d'un formulaire traditionnel.

Afin de répondre à ce besoin particulier, deux classes sont donc créées :

- GwtSavedRequestAwareAuthenticationSuccessHandler ;

- GwtSavedRequestAwareAuthenticationFailureHandler.

L'identification des requêtes GWT doit se faire sur un discriminant simple. L'approche proposée est de s'appuyer sur un header http spécifique. RequestBuilder va donc ajouter à la signature de la requête le header X-GWT-Secured=true.

Dans le cas d'une requête http issue du composant RequestBuilder ainsi configuré, les deux classes pourront ainsi adapter leur réponse en retournant des structures simples à interpréter (une simple chaîne JSON dans un cas, et une réponse avec un « status code » explicite dans l'autre).

Dans le cas d'un accès dit « involontaire », il convient de venir se greffer sur la classe ExceptionTranslationFilter de Spring Security. Cette classe s'appuie sur un AuthenticationEntryPoint pour proposer l'« accès » à la méthode d'authentification requise. Dans le cas d'un formulaire, c'est LoginUrlAuthenticationEntryPoint qui se charge d'effectuer une redirection vers la page du formulaire. Une telle redirection vers une page HTML contenant le formulaire d'authentification ne serait pas comprise par le composant GWT appelant et sera difficilement exploitable.

Aussi, un nouvel objet de type AuthenticationEntryPoint va devoir savoir lui aussi faire la distinction entre une requête issue d'un composant GWT et une requête autre et répondre dans le cas d'une requête issue de GWT quelque chose d'interprétable.

C'est le rôle que va jouer la classe GwtAwareAuthenticationEntryPoint en produisant une réponse JSON à toute requête reconnue comme issue de GWT. Le cas échéant, elle déléguera à LoginUrlAuthenticationEntryPoint.

La classe ExceptionTranslationFilter gérant aussi les exceptions de type AccessDeniedException, nous avons ainsi aussi la matière pour prendre en compte les cas de privilèges insuffisants du paragraphe suivant.

V - Contrôle des droits d'accès

Comme évoqué plus haut, le contrôle des droits d'accès ne peut pas se faire sur la simple base d'un filtre sur les URL. À ce niveau, seule une sécurité très macroscopique peut éventuellement trouver place (exemple : « il faut être connecté pour accéder à cette ressource »). Dès lors qu'on souhaite avoir un contrôle plus fin, du type : la suppression de la ressource X requiert un privilège DELETE_RESOURCE alors que sa consultation ne requiert que GET_RESOURCE, il convient de trouver une approche alternative plus fine.

Spring Security propose deux façons de procéder pour définir et contrôler les droits. Comme évoqué, l'approche par simple application d'un filtre sur l'URL est insuffisante sur des appels de type RPC. Par contre une sécurisation des services par instrumentation du code permet de répondre à ce problème d'offuscation de l'action demandée.

Comme vu précédemment, en se greffant sur ExceptionTranslationFilter, il est simple de prendre en compte le cas d'un accès refusé pour manque de privilège (réalisé aussi par la classe GwtAwareAuthenticationEntryPoint).

Ce qui reste à présenter maintenant, c'est la mécanique d'interception de ces réponses dans le code GWT, que cela soit dans le cas d'un appel GWT-RPC ou dans le cas d'un appel via RequestFactory.

Les deux approches partagent le même concept : un callback spécialisé va prendre en charge l'interprétation de ces réponses. Dans le cas de GWT-RPC, c'est la classe SecuredAsyncCallback qui va s'en charger, et SecuredReceiver pour RequestFactory.

Dans les deux cas, les méthodes onFailure() vont traquer une réponse liée à la sécurité et la gérer, sinon déléguer le traitement de l'erreur à une nouvelle méthode onServiceFailure() qui remplace le onFailure() par défaut.

Le code, pour un appel GWT-RPC, devient alors :

@UiHandler("gwtRPCButton")
public void onRpcButtonClick(ClickEvent e) {
ApplicationResourcesFactory.getSimpleService().getData(new SecuredAsyncCallback<String>() {
public void onServiceFailure(Throwable caught) {
// Show the RPC error message to the user
DialogBox dialogBox = new DialogBox();
dialogBox.center();
dialogBox.setModal(true);
dialogBox.setGlassEnabled(true);
dialogBox.setAutoHideEnabled(true);
dialogBox.setText("Remote Procedure Call - Failure");
dialogBox.show();
}
public void onSuccess(String result) {
DialogBox dialogBox = new DialogBox();
dialogBox.center();
dialogBox.setModal(true);
dialogBox.setGlassEnabled(true);
dialogBox.setAutoHideEnabled(true);
dialogBox.setText(result);
dialogBox.show();
}
});
}

VI - Autres fonctionnalités

Parmi les autres fonctionnalités, on peut ne peut pas oublier la déconnexion. Elle est tout aussi utile que la connexion et ne pas la proposer est une potentielle faille.

Spring Security propose un filtre simple pour répondre à ce besoin. Il suffit d'appeler une URL particulière, au besoin personnalisable, pour détruire le contexte de sécurité. Cela fonctionne très bien avec une authentification par formulaire ou par cookie (remember me). Par contre, une authentification de type BASIC (par exemple) sera automatiquement récréée à la prochaine requête et seule une fermeture du navigateur, en plus de ce mécanisme (à cause du potentiel cookie du remember me), peut avoir raison de l'authentification (à noter qu'il existe une autre approche moins radicale, mais aussi moins officielle, que la fermeture du navigateur, que je ne documenterai pas ici).

Afin de supporter cette fonction de déconnexion, il convient donc de créer un composant GWT dont la responsabilité est la création d'une requête http pointant sur cette URL de déconnexion. Rien de plus simple avec RequestBuilder. Voir pour cela le code de la classe com.gwtsecurity.client.ui.LogoutButton.

L'option souvent associée à l'authentification « classique » est la fonctionnalité de « remember me ». Cette option permet de stocker dans un cookie une information qui va permettre lors d'une visite ultérieure, et même après une expiration de session sur le serveur (c'est l'intérêt d'ailleurs), de restaurer un contexte de sécurité. Cette option est très intéressante, d'autant plus avec les applications GWT qui favorisent le travail déconnecté (la richesse locale de l'application permet de travailler sans interagir forcement avec le serveur).

Bonne nouvelle : il n'y a rien à faire de spécifique pour supporter cette fonctionnalité avec GWT, alors autant en faire usage. Il s'agit donc d'ajouter la configuration nécessaire à Spring Security pour activer cette fonctionnalité.

VII - Assemblage du tout

Cette configuration globale de la sécurité pour GWT nécessite quelques ajustements côté Spring Security. La configuration par défaut proposée par le namespace Spring Security ne peut plus suffire et il faut donc déclarer « à l'ancienne » la sécurité. Rien de compliqué lorsqu'on a cerné les responsabilités de chaque composant et le fonctionnement global, mais cela peut donner quelques maux de tête au démarrage. La configuration illustrée dans l'application d'exemple peut servir de base : /gwt-security-sample/src/main/resources/META-INF/spring/applicationContext-security.xml

VIII - Conclusion

La solution présentée mérite sûrement des améliorations ou adaptations (je vous invite à forker le projet), mais elle pose quelques bases qui pourront vous aider rapidement à mettre en place les composants nécessaires à la sécurisation de votre application.

Par ailleurs, si vous souhaitez approfondir le domaine de la sécurité dans les applications Java, je vous invite à (re)consulter le livre blanc sur le sujet : http://www.ippon.fr/livres-blancs.

IX - Remerciements

Cet article a été publié avec l'aimable autorisation de David Martin. L'article original peut être vu sur le blog/site de Ippon.

Nous tenons à remercier Torgar pour sa relecture orthographique attentive de cet article puis Mickael Baron et mlny84 pour la mise au gabarit.