TP 4 : Web APIs (programmation REST)
Objectifs pédagogiques du TP
- Comprendre les grands principes de l'approche REST
- Se familiariser avec la programmation orientée-ressource
- Concevoir et réaliser une API Web
- Utiliser une méthode d'authentification sans états
Références
Vous puiserez dans le cours de REST et en particulier, les références en fin de cours.
Préambule
Dans ce TP, vous repartirez du back-office de l'application de blog réalisée dans le TP précédent. Vous l'améliorerez pour faire en sorte que les objets métier de cette application soient gérés sous forme de ressources. Vous l'exposerez sous la forme d'une Web API.
Dans un premier temps, vous conserverez la méthode d'authentification utilisée dans le TP précédent (cookies + Filter). À la fin du TP, vous remplacerez cette méthode par l'utilisation de Json Web Tokens (JWT).
Outils
Vos utiliserez les outils suivants pour tester votre Web API :
Ressources
Au TP précédent, vous avez approfondi deux cas d'utilisation de l'application : la gestion des groupes et d'un billet d'un groupe. Le travail qui doit déjà être fait est que les ressources exposées par votre application (groupes, billets), ont chacune une URI qui lui est propre et que le serveur renvoie une réponse à une requête vers cette URI. De la même façon, vous avez une autre servlet (User), qui sera à terme destinée à gérer les utilisateurs.
Modification des URLs
Dans les annotations de vos contrôleurs et le fichier XML (pour les filtres), modifiez les URLs pour qu'elles correspondent à des URLs de ressources en REST. Procédez de la façon suivante :
- Définissez l'arborescence de vos ressources métier et l'ensemble des URLs qui leur correspond.
- Une fois vos URLs définies, le plus simple est de réaliser une servlet principale (front controller, ou routeur) que vous mapperez à la racine de votre application et qui sera responsable de l'aiguillage des requêtes vers les contrôleurs de cas d'utilisation. Pour cela, parsez les chemins de vos URLS (request.getRequestURI() peut vous y aider) et découpez les chemins pour récupérer :
- les noms génériques des ressources de type collection,
- les identifiants des instances de ressources.
Pour chaque type d'URL correspondant à un contrôleur, ajoutez les attributs nécessaires au traitement de la requête et faites une redirection interne vers ce contrôleur.
- Pour les filtres : le plus simple est d'intercepter toutes les URLs à partir d'un chemin donné (par exemple : /* pour le filtre d'authentification), et de définir une variable contenant l'ensemble des URLs à ignorer (chain.doFilter()).
Aide pour la réalisation du routeur :
- Dans le routeur, lorsque vous "passez la main" à un autre composant, utilisez un NamedDispatcher (et non un RequestDispatcher qui bouclera à l'infini).
- Utilisez la méthode forward() si ce composant est amené à modifier le code de statut ou les headers de la réponse. Cependant, une fois sorti de ce composant, le flux de sortie sera fermé et vous ne pourrez plus écrire dedans.
- Utilisez la méthode include() si ce composant ne modifie pas le code de statut ni les headers de la réponse (il peut en revanche écrire dans le flux de sortie) et si vous avez à réaliser des traitements ultérieurs après le retour du dispatcher (par exemple, de la négociation de contenus). "The included servlet cannot change the response status code or set headers; any attempt to make a change is [silently] ignored.". Par ailleurs, on peut écrire dans le flux de sortie depuis un composant invoqué avec include() mais si un autre composant écrit ultérieurement dans ce flux, les premières modifications seront perdues.
- En résumé : pour rediriger du routeur vers les contrôleurs délégués, vous utiliserez la méthode include() de NamedDispatcher. Les contrôleurs délégués, feront uniquement des appels au modèle et positionneront des attributs de requête. Vous utiliserez des attributs de requête pour informer le routeur de l'ajout de headers ou de la modification du code de statut / d'erreur, et ferez ces modifications dans le routeur.
Représentations des ressources
En REST, le client et le serveur échangent des représentations de ressources, éventuellement partielles. L'objectif de cette partie est de définir, comme cela a été précisé en cours, les contenus des requêtes et des réponses échangées par votre API. Concernant le format, pour l'instant, vous pouvez continuer à renvoyer uniquement des réponses en HTML (vous étendrez cela dans la question sur la négociation de contenus).
Contenus des requêtes
- Pour les requêtes portant du contenu dans le payload, définissez des formats d'objets (en XML ou JSON), qui décrivent les représentations partielles des ressources que vous souhaitez créer ou modifier.
- Parsez les contenus de ces requêtes dans vos contrôleurs et générez les objets du modèle correspondants
Contenus des réponses
Pour l'instant, vous pouvez laisser les JSP qui composent les réponses en HTML, mais assurez-vous qu'elles renvoient bien des contenus en lien avec les requêtes (par exemple la liste des groupes sur l'URL /groupes).
Sémantique de HTTP
Faites en sorte que :
- Les méthodes GET, POST, PUT et DELETE soient appelées à bon escient
- Seules les appels à la méthode GET renvoient du contenu
- Les codes de statut et d'erreurs correspondent à la leur sémantique en HTTP
Transactions sans état
Contexte des requêtes
Vous allez maintenant modifier le fonctionnement interne de l'application en faisant en sorte que le contexte des requêtes soit géré par le client et non pas en interne du serveur.
- Passez le CU de gestion des billets en push-based au lieu de pull-based.
- Faites en sorte que les noms des groupes (URL-encoded) et index des billets apparaissent dans les chemins des URLs
- Supprimez ces informations de la session utilisateur ; finalement, supprimez toute référence à la notion de session
- Testez
Remarque : pour l'instant, ne vous préoccupez pas des liens entre les ressources. Par exemple, le champ "auteur" d'un billet n'est qu'un String sans signification, et le serveur n'a pas à vérifier qu'il correspond réellement à un utilisateur.
Authentification Stateless
Pour cette partie, vous vous aiderez d'une bibliothèque externe : Java-JWT. Vous utiliserez cette bibliothèque (et sa documentation) pour remplacer l'authentification par session par de l'authentification par JWT. Pour cela :
- Ajoutez la dépendance Maven
- Dans le contrôleur du CU de login (servlet Init ou User), remplacez la création de session par celle d'un token "simple" JWT. Le payload ne contiendra que 2 "claims" :
- l'URI complète de l'utilisateur (claim = "sub" : subject)
- l'autorité qui a émis le token ("iss" : issuer)
- Dans un premier temps et pour pouvoir tester votre application, placez le token dans un cookie qui sera envoyé au client, et que celui renverra automatiquement au serveur à chaque nouvelle requête
- Dans le filtre d'authentification :
- accédez au token
- validez le token
- s'il est valide, rajoutez le login contenu dedans en attribut de requête et "laissez passer" la requête
- redirigez vers la page d'accueil (comme précédemment) sinon
- Rajoutez une durée de vie de la session (courte pour pouvoir tester) dans le payload, et faites en sorte qu'elle soit prise en compte dans la validation
Négociation de contenus
Vous allez maintenant permettre aux clients de spécifer le format dans lequel ils obtiendront des représentations de ressources :
- HTML : à l'aide des JSP que vous avez déjà réalisées + d'autres au besoin
- XML : vous pouvez aussi utiliser des JSP pour réaliser ces vues
- JSON (type renvoyé par défaut) : au choix, vous pouvez directement sérialiser des objets avec Jackson ou écrire la sérialisation dans une servlet
Pour améliorer la cohésion des contrôleurs et éviter la duplication de code, vous allez découpler le pattern MVC de la négociaton de contenus (voir plus bas), vous allez "couper" votre MVC en deux, à l'aide d'un pattern DTO et déléguer la responsabilité de la négociation de contenus à un filtre.
- regroupez les différents types de vue pour chaque ressource dans un dossier commun (sous-dossier de WEB-INF).
- dans les contrôleurs, positionnez un attribut de requête qui indique le nom du dossier contenant les différentes vues de la ressource
- mettez en place des Data Transfer Objects (DTO) qui pourront contenir les données des représentations de vos modèles
- dans les contrôleurs, instanciez un DTO contenant les données nécessaires et rajoutez-le en attribut de requête
- créez un filtre qui intercepte les requêtes après leur traitement par le contrôleur et dont la responsabilité sera de gérer la négociation de contenus
- dans le fichier web.xml, rajoutez des paramètres d'initialisation du filtre, pour indiquer le début du chemin d'accès à ce fichier, et éventuellement, les suffixes des fichiers qui contiennent les vues
- dans le filtre, accédez au header Accept de la requête, et en fonction de sa valeur, incluez la vue correspondante
Aide pour la génération de contenus HTML avec des JSP :
- Comme dans le routeur, utilisez un NamedDispatcher pour appeler les JSP depuis votre filtre
- Vous devez donc donner un nom à vos JSP dans le contexte applicatif ; cela se fait en déclarant les JSP comme des servlets dans le fichier web.xml et en utilisant les balises <servlet-name> et <jsp-file>
Ajout de liens
Faites en sorte que les représentations de chaque ressource contiennent des liens vers les ressources apparentées ; par exemple, la représentation du groupe devrait contenir un lien vers chaque billet, et les champs de type String représentant les auteurs d'un billet ou d'un commentaires devront être remplacés par les URI des ressources.
Lorsqu'une ressource a été créée, un lien doit être renvoyé (header HTTP Location de la réponse), contenant l'URI de cette nouvelle ressource.
Documentation de l'API
Logiquement, vous devriez également écrire la documentation de votre API. Pour vous faire gagner du temps (et à nous aussi pour la correction), cette documentation vous est fournie ici. Elle est décrite en OpenAPI, et utilisable dans Swagger. Vous pouvez la visualiser et tester votre API en ligne à l'URL http://editor.swagger.io/.
Remarque : pour pouvoir tester voter API depuis l'éditeur en ligne de Swagger, il faut que Tomcat l'autorise à lui envoyer des requêtes, à l'aide du mécanisme Cross-Origin Resource Sharing (CORS). Pour cela, vous devez rajouter la config ci-dessous (avant les autres filtres) dans le fichier web.xml.
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
<init-param>
<param-name>cors.allowed.origins</param-name>
<param-value>http://editor.swagger.io</param-value>
</init-param>
<init-param>
<param-name>cors.allowed.methods</param-name>
<param-value>GET,POST,PUT,DELETE</param-value>
</init-param>
<init-param>
<param-name>cors.support.credentials</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Finalisation de l'API
Faites en sorte que votre API corresponde à la description fournie à la question précédente.
Instructions de rendu
Vous tagguerez la dernière version pushée sur la forge de votre Web API en REST avec le tag "TP4", et vous déploierez cette version sur votre VM à l'URL https://192.168.75.xx/api/v2/ ; le dernier push ainsi taggué doit dater au plus tard du mardi 12 novembre 2019 au soir.