10 nov. 2021 14:55:35 Thomas Dumont avatar

Exposer une API REST avec Lutece

Introduction

Les services web de type REST (Representational state transfer) exposent entièrement leurs fonctionnalités comme un ensemble de ressources (URI) identifiables et accessibles par la syntaxe et la sémantique du protocole HTTP. Les Services Web de type REST sont donc basés sur l'architecture du web et ses standards de base : HTTP et URI. Les données fournies par ces services sont généralement disponibles en plusieurs formats : XML, JSON ou HTML.

Les opérations de CRUD sur une ressource sont réalisées à l'aide des méthodes HTTP : PUT (create), GET (read), POST (update), DELETE (delete).

Les avantages de ce type de webservices sont :

  • Ils sont utilisables par n’importe quelle brique (client riche ou mobile, autre SI, autre service…) dans n’importe quelle techno
  • Ils sont exposables via HTTP : API publique et humainement compréhensible
  • Ils sont scalables et cachables notamment par les équipements du web : proxys , caches.

Plusieurs frameworks sont disponibles pour faciliter la réalisation de services web REST. On peut citer notamment Restlet ou Apache CXF.

Pour l'implémentation par défaut proposée par Lutece, le framework Jersey a été retenu. Jersey est l'implémentation de référence de JAX-RS (JSR 311) qui désigne la spécification des web services RESTful.

Intégration dans Lutece : Le plugin REST

Le plugin REST offre une couche de service standard pour tous les modules REST. Il détecte automatiquement toutes les ressources dont la classe est déclarée dans le fichier contexte Spring du plugin ou du module. Cette opération est réalisée au lancement de la webapp et les ressources détectées sont affichées dans les logs.

Le plugin utilise ensuite un filtre de servlet, basé sur l'URI /rest et réalise le dispatching vers les URIs des ressources. Lorsque le plugin REST est déployé, il n'y a donc aucun développement ou paramétrage particulier à réaliser en dehors des classes des ressources à fournir et à déclarer dans les fichiers de contexte.

L'utilisation du plugin-rest nécessite donc 3 étapes:

  • Ajout de la dépendance vers pluging-rest dans le fichier pom.xml
  • Implémentation d'une classe représentant la ressource REST : voir exemple ci-dessous.
  • Déclaration de cette classe dans le fichier de contexte de spring (ex webapp/WEB-INF/conf/plugins/<plugin>_context.xml) : <bean id=.. class=..>

Les annotations Jersey

L'écriture de web services est grandement facilitée avec Jersey par le biais d'annotations.

Les principales annotations sont les suivantes :

Type de requête HTTP pris en charge par la méthode @GET, @PUT, @POST, @DELETE
Chemin de l'URI pris en charge par la méthode @Path( path )
Format de réponse demandé @Produces( format ) Exemples de format : MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML,...
Nature des données en entrée @Consumes( format ) Exemples de format : MediaType.APPLICATION_FORM_URLENCODED

Voir la documentation de JAX-RS

En voici un exemple :

@Path( RestConstants.BASE_PATH + "customer" )
public class CustomerRest
{
    public class CustomersResource 
    {
        @GET
        @Path( '/customers' )
        @Produces( MediaType.APPLICATION_XML )
        public List<Customer> getCustomers( ) 
        {
            ... 
        }

        @GET
        @Path( '/customers/{id}' )
        public Customer getCustomer( @PathParam( 'id' ) int nId ) 
        {
            ...
        }

        @PUT
        @Path( '/customers/add' )
        @Produces( MediaType.TEXT_PLAIN )
        @Consumes( MediaType.APPLICATION_XML )
        public String addCustomer( Customer customer ) 
        {
            ...
        }

        ...
    }
}

Normes et conventions de nommage

URI

La racine des URI se définit avec l'annotation @Path de la manière suivante :

@Path( RestConstants.BASE_PATH + Constants.PLUGIN_NAME )

Liste de ressources :

/rest/{myplugin}[/{mymodule}]/(myressources}/

Ressource :

/rest/{myplugin}[/{mymodule}]/(myressources}/{id}

Paramètres de la requête : sort, start, count (ou rows)

Java

  • La couche REST est packagée sous forme de module.
  • Nom du module : {myplugin}-rest
  • Nom des classes des ressources : {MyRessource}Rest.java
  • Nom du package contenant les classes des ressources : rs

Sécurité

La problématique de sécurisation des services web REST réside dans le fait qu'ils sont basés sur un protocole sans état. Chaque requête est indépendante et il n'y a pas de notion de session.

La sécurité, lorsqu'elle doit être mise en oeuvre, doit donc véhiculée au niveau de chaque requête. Les mécanismes les mieux adaptés sont ceux basés sur des signatures, des tokens ou des clés associés à chaque requête HTTP.

La librairie SignRequest

Cette librairie fournit une API pour définir des services d'authentification de requêtes HTTP : les RequestAuthenticator. Elle propose plusieurs implémentations dont notamment HeaderHashRequestAuthenticator qui permet de créer et valider une signature de la requête basée sur les paramètres de celle-ci et sur un secret partagé entre le client et le serveur. Si la signature est absente, incorrecte ou réalisé avec une mauvaise clé, la requête sera rejetée.

Cette librairie n'a pas de dépendance avec le core de Lutece. Elle peut donc être utilisée facilement par d'autres applications Java non-Lutece comme des applications Android par exemple.

Les implémentations d'authenticator et de filtre fournis par cette librairie peuvent répondre à de nombreuses situations mais sont aussi des exemples. Ils peuvent tout à fait être étendus ou modifiés en fonction des besoins

Les paramètres de HeaderHashAuthenticator

Cet authenticator doit être configuré à l'aide de plusieurs paramètres :

  • le service de hachage. La librairie SignRequest fournit une API de HashService et une implémentation utilisant l'algorithme SHA-1.
  • la clé privée correspondant au secret partagé entre le client et le serveur
  • la liste des paramètres de la requête qui sont utilisés pour composer la signature
  • la durée de validité de la signature en secondes. La valeur 0 indique que la durée n'est pas contrôlée.

Configuration d'un RequestAuthenticator dans le plugin REST

La sécurisation de l'ensemble des requêtes peut se faire au niveau du plugin REST en injectant via le context Spring un authenticator.

Par défaut, le plugin REST utilise l'implémentation NoSecurityRequestAuthenticator qui autorise l'ensemble des requêtes. L'exemple ci-dessous montre une configuration utilisant le HeaderHashRequestAuthenticator et son paramètrage spécifique.

<bean id='rest.hashService' class='fr.paris.lutece.util.signrequest.security.Sha1HashService' />
<bean id='rest.requestAuthenticator' class='fr.paris.lutece.util.signrequest.HeaderHashAuthenticator' >
    <property name='hashService' ref='rest.hashService' />
    <property name='signatureElements' > 
        <list>
            <value>key</value>
        </list>
    </property>
    <property name='privateKey'>
        <value>change me</value>
    </property>
    <property name='validityTimePeriod'>
        <value>0</value>
    </property>
</bean>

Un autre RequestAuthenticator dénommé RequestHashAthenticator est disponible dans la librairie. La seule différence est que la signature et le timestamp sont passés en paramètre de la requête HTTP au lieu de Headers de celle-ci. La signature est moins masquée, mais cela permet de sécuriser des requêtes réalisées par des liens hypertexte.

Configuration d'un filtre de servlet Lutece

Dans le fichier XML d'un module REST il est possible de déclarer un filtre qui fournira une sécurité spécifique aux ressources du module de la manière suivante :

<filters>
    <filter>
        <filter-name>myresourcesecurity</filter-name>
        <url-pattern>/rest/myresource/*</url-pattern>
        <filter-class>fr.paris.lutece.util.signrequest.servlet.HeaderHashRequestFilter</filter-class>
        <init-param>
            <param-name>elementsSignature</param-name>
            <param-value>id-resource,name,description</param-value>
        </init-param>
        <init-param>
            <param-name>validityTimePeriod</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>privateKey</param-name>
            <param-value>change me</param-value>
        </init-param>
    </filter>
</filters>