La connaissance des profiles Maven facilitera votre compréhension des explications techniques de ce billet.

Besoin de sécurité

Vue de l'utilisateur, mon application se compose d'une partie cliente et d'appels Rpc. La partie cliente, c'est essentiellement du code html et Javascript et d'autres ressources statiques comme du css ou des images. La partie cliente est vide de contenu au départ. Ce sont les appels Rpc qui vont remplir l'interface de données, que ce soit des données de référence ou des données à gérer.

On souhaite sécuriser l'application avec du SSO. La partie SSO sera active en Recette et production mais l'on souhaite la désactiver en mode développement afin de faciliter les tests de niveau developpeur.

Les utilisateurs sont gérés en base de données.

Technique de sécurisation répondant au besoin

La partie cliente statique ne contenant pas de données, il n'est pas nécessaire de la sécuriser, un utilisateur lambda ne fera rien de quelques lignes html et Javascript. Un utilisateur mal intentionné et ayant des connaissances poussés en informatique ne pourra tout au plus que trouver des adresses Http d'appels Rpc.

Filtre Http

Il faut donc sécuriser les appels Rpc. Pour simplifier nous allons utiliser un filtre web sur l'url-pattern /*. Ainsi nous sécurisons l'ensemble des requêtes.

Un paramètre sur ce filtre nous permettra d'activer ou pas le mode SSO. Ce paramètre sera géré par Maven dans un profile Maven.

Si le mode SSO est activé, le filtre de sécurité utilisera l'API du SSO afin de valider si l'utilisateur est connecté. Si la connexion SSO est valide, dans ce cas on récupère l'identifiant nécessaire (via l'API du SSO) pour récupérer l'utilisateur en base et on le stock en session Http. Sur le prochain appel Rpc, si l'utilsiateur est en session pas besoin de le récupérer à nouveau de la BDD.

Dans un précédent billet j'ai conseillé de stocker les objets dans un cache directement côté client. Ce conseil est important afin de garder le serveur en mode State-less. Ce conseil doit être appliqué à la lettre pour tous les objets sauf pour l'objet qui stocke les droits de l'utilisateur. Dans le domaine de la sécurité et une architecture client / serveur, on ne doit pas faire confiance au client. L'objet Utilisateur qui contient les droits peut être envoyé au client (ou navigateur) si on en a besoin pour afficher ou faire disparaitre des boutons en fonctions de droits. Mais un mécanisme d'autorisation doit également être mis en place au niveau des services métier de l'application. Il ne s'agit pas d'un doublon. D'un côté on fait disparaitre des boutons qu'un utilisateur n'a pas le droit d'utiliser, Ca n'est pas un niveau de sécurité mais une amélioration ergonomique. Et de l'autre côté on vérifie au niveau du service appelé par le dit bouton si l'utilisateur à bien les droits de faire appel à ce service.

Si le mode SSO n'est pas activé on pourra utiliser un bouchon quelconque pour définir un utilisateur par défaut avec des droits par défauts.

Annotations pour sécuriser les services

J'ai mis en place des annotations sur les services métier afin d'y définir un niveau de sécurité. Pour faire simple, un utilisateur à un droit en modification qui est en fait une variable boolean. Tous les services qui font des modifications sur la base de données vont avoir une annotation RolesAllowed qui va définir un niveau MODIFICATION.

On utilisera l'AOP pour vérifier sur chaque appel à un service que l'utilisateur courant à bien le droit défini dans l'annotation. Pour se faire, l'introspéctions des classes permet de rechercher les annotations sur les méthodes appelées. Si l'on trouve une telle annotation on vérifie si l'utilisateur courant, que l'on a stocké dans un Thread local et que l'on retrouve facilement via un Helper, a les bon droits. Si ce n'est pas le cas on lance une exception que l'on prendra soin de récupérer correctement afin d'afficher un message clair sur l'IHM.

Exemple technique

Voici un exemple de contenu du fichier web.xml afin de mettre en place un filtre Http sur toutes les requêtes.

<filter>
	<filter-name>SecurityFilter</filter-name>
	<filter-class>
		le.package.de.mon.application.server.base.security.SecurityFilter</filter-class>
	<init-param>
		<param-name>SSO_MODE</param-name>
		<param-value>false</param-value>
	</init-param>
</filter>	
<filter-mapping>
	<filter-name>SecurityFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

Je ne vais pas mettre le codes d'exemple complet de la servlet qui est assez basique à réaliser et qui prendrait trop de lignes inutiles sur ce billet. Les étapes à suivre sont :

  • Vérifier l'état de la variable SSO_MODE comme dans la méthode init ci-dessous
  • Récupérer l'identifiant de l'utilisateur soit via SSO, soit via un bouchon du mode dev
  • Récupérer l'objet, soit de la session http, soit de la BDD, représentant les autorisations de l'utilisateur et le mettre dans le thread local. On pourra ainsi le récupérer plus tard dans le thread courant.
public void init(FilterConfig filterConfig) throws ServletException {
	this.filterConfig = filterConfig;
	this.ssoMode = false;
	String mode = this.filterConfig
			.getInitParameter(ISecurityFilter.SSOMODE);
	if (mode != null && Boolean.TRUE.equals(Boolean.valueOf(mode))) {
		this.ssoMode = true;
	}
}

Ensuite mettre une annotation sur l'interface de persistance comme ceci :

public interface IService <T> {
  @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
  @RolesAllowed(value = {IAutorisation.MODIFICATION})
  T persist(final T bean);

...
}

Et puis un exemple de code d'interceptor AOP pour introspecter les appels aux services métier et vérifier que l'utilisateur a bien les droits :

import org.aopalliance.intercept.MethodInterceptor;

public final class ServiceHandlerInterceptor implements MethodInterceptor {
...
  public Object invoke(final MethodInvocation pMethod)
    throws BasicException {
    Object object = null;
    try {
      Method ins = pMethod.getMethod();
      this.checkAutorisation(ins);

      // On peut également en profiter checker les validator hibernate
      this.validateEntities(pMethod.getArguments());

      object = pMethod.proceed();
      // Traitement generique des exceptions
    } catch (Throwable vThrowable) {
      throw ExceptionHelper.transform(vThrowable);
    }
    return object;
  }

  private void checkAutorisation(Method ins) throws BasicException {
    // On récupère l'utilisateur du Thread local
    Utilisateur user = UserHelper.getCurrentUser();
		
    if (ins != null) {
      // On introspecte pour vérifier s'il y a une annotation RolesAllowed 
      // sur la méthode intercepté par AOP
      RolesAllowed r = ins.getAnnotation(RolesAllowed.class);
      if (r != null) {
        if (user == null) {
          // Si on a une annotation et pas d'utilisateur dans le Thread local 
          // c'est qu'il y a un problème
          throw new BasicException("No user");
        }
        Autorisation auth = user.getProfile().getAutorisations();
        // Pour chaque autorisation trouvé dans l'annotation on vérifie 
        // que ca correspond bien aux droits de l'utilsiateur		
        for (String s : r.value()) {
          if (!this.isAuthorized(auth, s)) {
            throw new BasicException("Action non autorisée");
          }
        }
      }
    }
  }
...
}

Et voilà, avec ces explications et ces quelques lignes de codes en exemple, vous devriez pouvoir mettre en place ce type de système de sécurité.