Introduction▲
On appellera un Service Web RESTful si il englobe la grande majorité des bons conseils de Roy T. Fielding, l'inventeur de REST.
- HTTP
- Simplicité
- Adressabilité
- Connectivité
- Absence d'Etat ou de Session du serveur (Stateless)
- Les URI définissent des Ressources
Comme tout autre Service Web, la sécurité des données est importante. Les mauvaises langues diront que les Ressources d'un service SOAP sont hautement sécurisées puisque impossibles à atteindre.
I. Architecture Globale▲
Le Client envoit une requête au conteneur JSP (ici, Tomcat 6.0.x). La page JSP va transmettre au RessourceController l'URI de la requête, les paramêtres,
ainsi que le PostBody de la requête.
La page JSP dirigera le travail effectué par le RessourceController selon la méthode Http utilisée.
Notes : - Le PostBody, par opposition à la QueryString, constitue les paramètres de la requête que l'on ne voit pas dans l'URL. - Un Controller, dans la terminologie REST, est la classe coté serveur chargée de spécifier le fonctionnement d'une Ressource vis-à-vis d'une requête. Ce sera souvent un JavaBean associé à une JSP et d'autres classes métier. - Un RessourceController est une interface implémentée par un JavaBean (notre Controller) définissant des fonctions fortement liées à une Ressource, telles que setURI() et setPostBody(). |
II. Récupérer les données de la requête▲
II-1. Récupérer les paramètres▲
C'est la partie classique et facile. Soit une requête PUT /wsxseditor/service/document/document.jsp?dataBaseId=24&title=ChangeTheTitle Il suffit d'intégrer dans la page jsp :
<jsp
:
useBean
id=
"controller"
class=
"controller.documentController"
scope=
"request"
/>
<jsp
:
setProperty
name=
"controller"
property=
"*"
/>
dataBaseId et title sont des attributs de votre classe DocumentController. Le DocumentController doit implémenter les fonctions setDataBaseId() et setTitle() et le moteur des JSP fait automatiquement le mapping.
II-2. Récupérer l'URI de la requête▲
Chaque JSP peut manipuler l'objet HttpServletRequest en utilisant ${pageContext.request}. Voici ce que l'on ajoute à la JSP :
<jsp
:
setProperty
name=
"controller"
property=
"URI"
value=
"${pageContext.request.requestURI}"
/>
DocumentController doit maintenant implementer l'interface RessourceController, ou au moins la fonction setURI().
II-C. Récupérer le PostBody de la requête▲
Voici une requête POST vue par le sniffer Wireshark. Notez qu'une requête POST peut très bien avoir une QueryString, ce qui peut être utile pour l'algorithme utilisant cete requête.
Dans cet exemple classique, je crée un nouvel élément dans mon document. Cet élément est représenté en XML dans le PostBody, et j'utilise dans la QueryString les paramètres fatherId et brotherId pour savoir où placer l'élément. Mais récupérer le PostBody est un peu plus délicat car il faut jongler avec les Streams de Java.
<%
java.io.BufferedReader br =
new
java.io.BufferedReader
(
new
java.io.InputStreamReader
(
request.getInputStream
(
)));
String line,result=
""
;
while
((
line =
br.readLine
(
)) !=
null
) {
result+=
line;
}
br.close
(
);
controller.setPostBody
(
result);
%>
Le DocumentController doit implémenter l'interface RessourceController, ou au moins la fonction setPostBody().
III. Traiter les différentes méthodes Http▲
L'utilisation des méthodes Http GET (Read), POST (Create), PUT (Update) et DELETE (Delete) est, à mon avis, l'aspect fondamental d'un Service Web RESTful. Pour connaitre la méthode utilisée, il faut encore faire appel à HttpServletRequest :
<c
:
if
test=
"${pageContext.request.method=='POST' }"
>
<root>
<c
:
choose
>
<c
:
when
test=
"${controller.createOK}"
>
<id>
controller.id</id>
<!-- creating a ressource add a row in the database-->
</c
:
when
>
<c
:
otherwise
>
<error>
controller.message</error>
</c
:
otherwise
>
</c
:
choose
>
</c
:
if
>
<c
:
if
test=
"${ pageContext.request.method == 'PUT' }"
>
<c
:
choose
>
<c
:
when
test=
"${controller.updateOK}"
>
<ok/>
<!-- a simple update -->
</c
:
when
>
<c
:
otherwise
>
<error>
controller.message</error>
</c
:
otherwise
>
</c
:
choose
>
</c
:
if
>
</root>
Le RessourceController implémentera selon les possibilités du Service Web les fonctions isReadOK(), isCreateOK(), is UpdateOK(), isDeleteOK().
IV. Analyser les URI▲
IV-1. Objectifs▲
Nous avions démarré l'article avec cette requête : PUT /wsxseditor/service/document/document.jsp?dataBaseId=24&title=ChangeTheTitle Nous voudrions une requête plus sexy comme celle-ci : PUT /wsxseditor/document/24?title=ChangeTheTitle
Cette écriture d'URL est moins importante que l'utilisation correcte des méthode HTTP, mais favorisera l'échange d'URL dans les différentes pages Web, et donc la Connectivité qui est bien sûr un concept essentiel du Web. L'écriture d'URL sexy permettra également, contrairement à SOAP, de comprendre au premier coup d'oeil ce que signifie la requête ce qui est très utile dans la maintenance du service.
IV-2. Servlet-Mapping▲
C'est très facile avec Netbeans de transférer une requête /wsxseditor/document/24 vers la JSP /wsxseditor/service/document/document.jsp. Double-cliquez sur le fichier web.xml, et remplissez ce formulaire :
Le fichier web.xml ressemblera à cela :
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
<servlet>
<servlet-name>
UriAdaptor</servlet-name>
<jsp-file>
/service/document/document.jsp</jsp-file>
<load-on-startup>
1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>
UriAdaptor</servlet-name>
<url-pattern>
/document/*</url-pattern>
</servlet-mapping>
</web-app>
Par contre, puisque le paramètre dataBaseId a disparu, il faut faire comprendre à notre RessourceController que le nombre 24 correspond au paramètre dataBaseId.
IV-3. Analyse de l'URI▲
Le ResourceController implemente la fonction setURI (). Quand nous transmettrons l'URI au Controller, on en profitera pour la décoder :
public
void
setUri
(
String uri) throws
NotFoundException {
this
.uri =
uri;
robusta.rest.j2ee.UrlDecoder decoder=
new
robusta.rest.j2ee.UrlDecoder
(
);
this
.dataBaseId=
decoder.firstNumber
(
uri);
}
UrlDecoder.firstNumber() est une fonction de ma Robusta ToolBox qui utilise des expressions régulières pour détecter le premier nombre. Vous trouverez le code source à cette adresse ou dans le zip en annexe.
V. Plus d'élégance avec un Tag JSP▲
Les scripts JSP ( <% java code %> ) ne sont pas très lisibles et, pire, favorisent le copier/coller du code source. On peut cependant utiliser
un tag JSP simple et non-intrusif pour résoudre ces problèmes.
Voici le fichier /WEB-INF/tags/request.tag :
<%@tag
description=
"Conciliate RESTful Web Services and JSPs - public domain - By Nicolas Zozol"
pageEncoding=
"UTF-8"
%>
<%@
attribute
name
=
"method"
%>
<%@
attribute
name
=
"ressource"
rtexprvalue
=
"true"
type
=
"base.RessourceController"
required
=
"true"
%>
<%@
attribute
name
=
"request"
rtexprvalue
=
"true"
type
=
"javax.servlet.http.HttpServletRequest"
required
=
"true"
%>
<%
java.io.BufferedReader br =
new
java.io.BufferedReader
(
new
java.io.InputStreamReader
(
request.getInputStream
(
)));
String line,result=
""
;
while
((
line =
br.readLine
(
)) !=
null
) {
result+=
line;
}
br.close
(
);
ressource.setPostRequest
(
result);
ressource.setURI
(
request.getRequestURI
(
));
this
.method=
request.getMethod
(
);
%>
Le Tag nécessite le RessourceController et la HttpServletRequest, puis crée la variable method. La page JSP ressemblera finalement à :
<%@
taglib
tagdir=
"/WEB-INF/tags/"
prefix
=
"robusta"
%>
<jsp
:
useBean
id=
"controller"
class=
"controller.documentController"
scope=
"request"
/>
<jsp
:
setProperty
name=
"controller"
property=
"*"
/>
<robusta
:
request
ressource=
"${controller}"
request=
"${pageContext.request}"
/>
<c
:
if
test=
"${method == 'POST' }"
>
... some xml ...
</c
:
if
>
Votre controller devra impérativement implémenter l'interface RessourceController afin de recevoir l'URI et le PostBody.
VI. Identifier l'utilisateur avec un Cookie ou des Credentials▲
Il s'agit maintenant de protéger les Ressources afin d'éviter que le tout venant n'éxecute une requête DELETE /users/58. Il y a évidemment plusieurs méthodes pour identifier l'individu. Mais la plupart d'entre elles utilisent soit un cookie, soit le header Authorization:mode credential.
VI-1. Pourquoi un Cookie ?▲
On utilisera en général le cookie pour lire des données qui ne sont pas dangereuses ni ultra-confidentielles. En principe l'utilisateur est responsable de son PC, ou doit cliquer sur le bouton deconnexion sur un PC public. Si il ne le fait pas, un intrus pourra lire le cookie. Par contre Wikipédia utilise par exemple le cookie pour afficher discrètement en haut à gauche des informations sur l'utilisateur en cours. Pour cela, Wikipédia envoie le cookie :
Cookie: frwikiUserName=Nicorama; frwiki_session=4f64774a3fkskfea0b2cbc4b6779fc94; frwikiUserID=52490; frwikiToken=abdf769sfcafhbb3c4c719cbdb8950a\r\n
En cliquant d'un lien à un autre sur Wikipédia, le DIV correspondant au user reste. Mais l'URL affichée dans le browser va changer, et vous pouvez envoyer par mail cette adresse URL à un ami si vous souhaitez entamer un debat. Le cookie est un moyen simple de garder l'Adressabilité de la page Web, ce qui facilite aussi le référencement dans les moteurs de recherches. Notez que la page est lisible en http et non https, ce qui est une raison de plus pour n'accéder qu'à des données non confidentielles.
Pour en savoir plus, voici un lien vers un blog en anglais traitant des "best practices" dans l'utilisation d'un cookie.
VI-2. Pourquoi les Credentials ?▲
On appelle les Credentials la valeur du header http Authorization : BASIC fdhqjkdkjq==
Supposons maintenant que vous vouliez effacer un utilisateur avec une requête DELETE /users/58. En cliquant sur un lien <a> classique ou en rentrant une adresse URL dans votre browser, vous n'obtiendrez que des GET. Il faut donc programmer cette requête, en utilisant en général Ajax (1). Lors de cette requête, insérer des credentials permet d'identifier l'utilisateur en se passant de cookies (que l'on a vu peu fiables).
Il est cependant nécessaire d'avoir une méthode de génération différente entre le cookie et le credential (sinon un intrus sur votre PC pourrait (2) lire le cookie puis accéder aux données confidentielles), et il faudra également passer par https afin de crypter les credentials.
Voici un exemple d'implémentation d'une telle requête avec Prototype :
new Ajax.Request
(
"/users/58"
,
{
method
:
"DELETE"
,
requestHeaders
:[
"Authorization"
,
"BASIC fjqkks678dd=="
],
onCreate
:
function(
){},
onSuccess
:
function(
){
/*OK*/
},
onComplete
:
function(
transport){
return transport;
},
onException
:
function(
request,
exception){
alert
(
"xhr -- Exception !:"
+
exception.
message);
},
onFailure
:
function(
){
/*FAIL*/
}
}
);
Pour exécuter cette requête, il faut que Javascript connaisse les credentials. En passant d'une page web à une autre, Javascript va perdre cette donnée puisqu'il est hors de question de passer par un cookie.
En utilisant les credentials dans une application Ajax, on perd donc en général l'Adressabilité.
VI-3. Architecture multi-tiers▲
Dans ce chapitre, je suppose que c'est le navigateur qui envoit des requêtes vers le service web. Il est cependant possible et même fréquent d'utiliser un serveur pour la couche présentation (JSF, PHP ou autre) envoyant des requêtes vers le service web afin de nourrir cette présentation. C'est ce que l'on a toujours fait avant Ajax.
Dans ce cas là, le serveur de présentation n'est pas prédestiné à créer et envoyer des cookies vers le service web. Si il reçoit un cookie du navigateur, il doit le convertir en credential, ou éventuellement rediriger tel quel le cookie.
VI-4. Le Tag JSP récupérant les credentials▲
Notre RessourceController va maintenant implémenter les fonctions setCredentials() et getCredentials(). Le tag va automatiquement récupérer ces données à la volée.
<%
/* Suite du tag robusta:request */
/* Credentials read in the header : Authorization:Basic xxxyyy or in the cookie : Authorization:Basic-xxxyyy
* In your cookies, you will use sometimes myToken instead of Authorization associated to Basic. The implementation below will change
*/
boolean
credentialsFound =
false
;
//priority is to the header over the cookie : if there are credentials in the header "Authorization", we don't care of the cookie
if
(
request.getHeader
(
"Authorization"
) !=
null
&&
!
request.getHeader
(
"Authorization"
).equals
(
""
)) {
ressource.setCredentials
(
request.getHeader
(
"Authorization"
));
credentialsFound =
true
;
}
else
{
//This is my implementation to read into the cookie.
javax.servlet.http.Cookie[] cookieList =
request.getCookies
(
);
for
(
int
i =
0
; i <
cookieList.length; i++
) {
if
(
cookieList[i].getName
(
).toLowerCase
(
).contains
(
"authorization"
)) {
ressource.setCredentials
(
cookieList[i].getValue
(
));
credentialsFound =
true
;
}
break
;
}
}
}
%>
Avec cette partie de tag, vous pourrez récupérer le "code d'authentification" en utilisant soit le cookie, soit le header Authorization. Cependant votre code métier doit gérer ces différents modes puisque, pour un même utilisateur, le code d'authentification doit impérativement être différent dans les deux cas.
VIII. Le Status Code de Réponse▲
Les adeptes de REST aiment utiliser un large éventail des codes retournés par le serveur, bien au-delà de 200, 404 ou 500. Ce petit tag permet de le faire aisemment.
Dans le fichier /WEB-INF/tags/response.tag :
<%@tag
description=
"Set datas to the response"
pageEncoding=
"UTF-8"
%>
<%@
attribute
name
=
"statusCode"
rtexprvalue
=
"true"
type
=
"Integer"
required
=
"false"
%>
<%@
attribute
name
=
"response"
rtexprvalue
=
"true"
type
=
"javax.servlet.http.HttpServletResponse"
required
=
"true"
%>
<%@
attribute
name
=
"ressource"
rtexprvalue
=
"true"
type
=
"robusta.rest.RessourceController"
required
=
"true"
%>
<%-- any content can be specified here e.g.: --%>
<%
if
(
statusCode >
0
) {
response.setStatus
(
statusCode);
}
else
{
if
(
ressource!=
null
&&
ressource.getStatusCode
(
) >
0
) {
response.setStatus
(
ressource.getStatusCode
(
));
}
}
%>
On peut alors insérer dans la JSP :
<%@
taglib
tagdir=
"/WEB-INF/tags/"
prefix
=
"robusta"
%>
<jsp
:
useBean
id=
"controller"
class=
"controller.documentController"
scope=
"request"
/>
<jsp
:
setProperty
name=
"controller"
property=
"*"
/>
<robusta
:
request
ressource=
"${controller}"
request=
"${pageContext.request}"
/>
<c
:
if
test=
"${method == 'POST' }"
>
... some xml ...
</c
:
if
>
<robusta
:
response
statusCode=
"212"
ressource=
"${controller}"
response=
"${pageContext.response}"
/>
Affichez la ligne <robusta:response> à la fin de la JSP, sinon votre serveur prendra le dessus. Vous pouvez utiliser l'attribut optionnel statusCode afin de noter directement le status, ou utiliser l'attribut également optionnel ressource - dans ce cas, vous devez coder dans votre Controller les différentes possibilités de statusCode car le tag appellera la fonction getStatusCode().
Conclusion▲
L'utilisation de cette méthode a un avantage immense vis-à-vis des Servlets : elle évite les affreux out.println("\"Me voilà\" dît Baudelaire"); mais intègre les URI, PostBody et méthodes Http de façon bien plus simple que ne le faisaient traditionnellement les Servlets. La page JSP sera parfaite pour échanger des données XML avec une application cliente Ajax ou avec la couche graphique de votre application.
JSF peut bien sûr faire le même travail, mais est plus lent et surtout plus compliqué à apprendre. Comme JSF est surtout dédié à la couche graphique du serveur cela ne vous aidera pas beaucoup pour créer un Service Web. Bien que les applications Ajax délèguent maintenant une bonne partie de la couche graphique au client Javascript, vous pouvez faire une application JSF qui lancera des requêtes vers votre service web JSP afin d'afficher ces données dans un composant JSF.
Le code des tags est sous domaine public afin d'être utilisé dans tout type de projets - à condition de spécifier mon nom dans le code. Si vous apportez des astuces ou améliorations non liées à votre code métier, n'hésitez pas à me les transmettre :). La bibliothèque Robusta est sous licence GPL 2.
Annexes▲
Une version de Prototype adaptée▲
Prototype transforme les requêtes DELETE et PUT en méthode GET, mais en rajoutant le paramètre _method. Voici le lien vers une version de Prototype effectuant réellement les méthodes PUT et DELETE - ce qui ne sera pas possible dans les navigateurs tels que Konquerors ou quelques navigateurs anciens ou exotiques.
L'interface RessourceController▲
Voici la version que j'utilise. On peut rajouter des fonctions permettant par exemple de s'assurer qu'un utilisateur ne fasse pas plusieurs POST en une minute.
package
robusta.rest;
/**
* A RessourceController is implemented by Controllers - you can then design nice customs JSP tags
*
@author
Nicolas Zozol - Edupassion.com - Robusta Web - nzozol@edupassion.com
*/
public
interface
RessourceController {
public
void
setURI
(
String uri);
public
void
setPostBody
(
String postBody);
/* Functions for differents http methods */
public
boolean
isReadOK
(
);
public
boolean
isCreateOK
(
);
public
boolean
isUpdateOK
(
);
public
boolean
isDeleteOK
(
);
/* StatusCode and optional message sent */
public
int
getStatusCode
(
);
public
void
setStatusCode
(
int
statusCode);
public
String getMessage
(
);
/* security : you can extend the interface or use Utility Classes */
public
void
setCredentials
(
String credentials);
public
String getCredentials
(
);
}
La ToolBox Robusta▲
La bibliothèque Robusta présente plusieurs packages qui sont tous indépendants (si ce n'est qu'ils utilisent parfois le package exception). Pour l'instant, elle est en travail intensif, et ne doit être utilisée que pour l'inspiration (sous Licence GPL 2.0).
Remerciements▲
Merci à Florian Casabianca pour la critique technique de l'article et Diogène pour sa relecture.