Skip to content

4. Sessie-tracking

4.1. Het probleem

Een webapplicatie kan bestaan uit meerdere uitwisselingen van formulieren tussen de server en de client. De werking is dan als volgt:

  • stap 1
    • de client C1 opent een verbinding met de server en doet zijn eerste verzoek.
    • De server stuurt het formulier F1 naar de client C1 en verbreekt de in stap 1 geopende verbinding.
  • stap 2
    • De client C1 vult het formulier in en stuurt het terug naar de server. Hiervoor opent de browser een nieuwe verbinding met de server.
    • De server verwerkt de gegevens van formulier 1, berekent op basis daarvan de informatie I1, stuurt een formulier F2 naar de client C1 en sluit de in stap 3 geopende verbinding.
  • stap 3
    • De cyclus van stap 3 en 4 herhaalt zich in stap 5 en 6. Aan het einde van stap 6 heeft de server twee formulieren ontvangen, F1 en F2, en heeft hij op basis daarvan de gegevens I1 en I2 berekend.

De vraag is: hoe zorgt de server ervoor dat de gegevens I1 en I2 die betrekking hebben op de klant C1, bewaard blijven? Dit probleem wordt de sessiebewaking van de klant C1 genoemd. Om de oorzaak ervan te begrijpen, bekijken we het schema van een servertoepassing TCP-IP die gelijktijdig meerdere klanten bedient:

In een klassieke client-server-applicatie TCP-IP:

  • maakt de client een verbinding met de server
  • wisselt via deze verbinding gegevens uit met de server
  • wordt de verbinding door een van beide partijen verbroken

De twee belangrijke punten van dit mechanisme zijn:

  1. voor elke client wordt één verbinding tot stand gebracht
  2. deze verbinding wordt gebruikt gedurende de gehele dialoog tussen de server en zijn client

Wat de server in staat stelt om op een bepaald moment te weten met welke client hij werkt, is de verbinding, of met andere woorden de "leiding" die hem met zijn client verbindt. Aangezien deze leiding aan een bepaalde client is toegewezen, is alles wat via deze leiding binnenkomt afkomstig van die client en komt alles wat via deze leiding wordt verzonden bij de client aan.

Het client-servermechanisme HTTP volgt het bovenstaande schema nauwkeurig, met echter als bijzonderheid dat de client-servercommunicatie beperkt is tot één enkele uitwisseling tussen de client en de server:

  • de client opent een verbinding met de server en doet zijn verzoek
  • de server geeft zijn antwoord en verbreekt de verbinding

Als op tijdstip T1 een klant C een verzoek indient bij de server, krijgt hij een verbinding C1 die zal dienen voor de eenmalige uitwisseling van verzoek en antwoord. Als op tijdstip T2 diezelfde klant een tweede verzoek aan de server doet, krijgt hij een verbinding C2 die verschilt van de verbinding C1. Voor de server is er dan geen verschil tussen dit tweede verzoek van gebruiker C en zijn oorspronkelijke verzoek: in beide gevallen beschouwt de server de klant als een nieuwe klant. Om een verband te leggen tussen de verschillende verbindingen van klant C met de server, moet klant C door de server worden „herkend“ als een „vaste klant“ en moet de server de informatie ophalen die hij over deze vaste klant heeft.

Laten we ons een systeem voorstellen dat als volgt zou werken:

  • Er is één enkele wachtrij
  • Er zijn meerdere loketten. Er kunnen dus meerdere klanten tegelijkertijd worden bediend. Wanneer er een loket vrijkomt, verlaat een klant de wachtrij om bij dat loket te worden bediend
  • Als de klant voor het eerst komt, geeft de medewerker aan het loket hem een wachtnummer. De klant mag slechts één vraag stellen. Zodra hij zijn antwoord heeft gekregen, moet hij het loket verlaten en achteraan in de wachtrij gaan staan. De loketmedewerker noteert de gegevens van deze klant in een dossier met zijn nummer.
  • Wanneer de klant weer aan de beurt is, kan hij door een andere loketmedewerker worden geholpen dan de vorige keer. Deze vraagt om zijn fiche en haalt het dossier met het nummer van de fiche op. Opnieuw stelt de klant een vraag, krijgt hij een antwoord en wordt er informatie aan zijn dossier toegevoegd.
  • En zo verder... Na verloop van tijd zal de klant op al zijn verzoeken een antwoord hebben gekregen. De samenhang tussen de verschillende verzoeken wordt bijgehouden aan de hand van het wachtnummer en het bijbehorende dossier.

Het mechanisme voor sessieopvolging in een client-server-webapplicatie werkt op dezelfde manier:

  • bij zijn eerste verzoek krijgt een klant een token van de webserver
  • hij zal dit token bij elk van zijn volgende verzoeken tonen om zich te identificeren

Het token kan verschillende vormen aannemen:

  • een verborgen veld in een formulier
    • de client doet zijn eerste verzoek (de server herkent hem aan het feit dat de client geen token heeft)
    • de server stuurt een antwoord (een formulier) en plaatst het token in een verborgen veld daarin. Op dat moment wordt de verbinding verbroken (de klant verlaat de server met zijn token). De server heeft er eventueel voor gezorgd dat er informatie aan dit token is gekoppeld.
    • de klant doet zijn tweede verzoek door het formulier terug te sturen. De server haalt daaruit het token op. Hij kan dan het tweede verzoek van de klant verwerken en heeft dankzij het token toegang tot de informatie die bij het eerste verzoek is berekend. Er wordt nieuwe informatie toegevoegd aan het dossier dat aan het token is gekoppeld, er wordt een tweede antwoord naar de klant gestuurd en de verbinding wordt voor de tweede keer verbroken. Het token is opnieuw in het antwoordformulier opgenomen, zodat de gebruiker het bij zijn volgende verzoek kan overleggen.
    • En zo verder...

Het grootste nadeel van deze techniek is dat het token in een formulier moet worden geplaatst. Als het antwoord van de server geen formulier is, kan de methode met het verborgen veld niet meer worden gebruikt.

  • die van de cookie
    • de client doet zijn eerste verzoek (de server herkent dit aan het feit dat de client geen token heeft)
    • de server geeft een antwoord door een cookie toe te voegen aan de headers HTTP van het antwoord. Dit gebeurt met behulp van het commando HTTP Set-Cookie:

Set-Cookie: param1=waarde1;param2=waarde2;....

waarbij parami de namen van de parameters zijn en valeursi hun waarden. Onder de parameters bevindt zich het token. Vaak bevat de cookie alleen het token, terwijl de overige informatie door de server wordt opgeslagen in de map die aan het token is gekoppeld. De browser die de cookie ontvangt, slaat deze op in een bestand op de harde schijf. Na het antwoord van de server wordt de verbinding verbroken (de client verlaat de interface met zijn token).

  • (vervolg)
    • de client doet zijn tweede verzoek aan de server. Telkens wanneer er een verzoek aan een server wordt gedaan, kijkt de browser tussen alle cookies die hij heeft, of er een is afkomstig van de aangevraagde server. Zo ja, dan stuurt hij deze naar de server, altijd in de vorm van een HTTP-commando, het Cookie-commando dat een syntaxis heeft die vergelijkbaar is met die van het Set-Cookie-commando dat door de server wordt gebruikt:

Cookie: param1=waarde1;param2=waarde2;....

Onder de parameters die door de browser worden verzonden, vindt de server het token waarmee hij de client kan herkennen en de bijbehorende informatie kan ophalen.

Dit is de meest gebruikte vorm van token. Het heeft echter een nadeel: een gebruiker kan zijn browser zo instellen dat deze geen cookies accepteert. Dit type gebruiker heeft dan geen toegang tot webapplicaties die gebruikmaken van cookies.

  • herschrijving van URL
    • de klant doet zijn eerste verzoek (de server herkent hem aan het feit dat de klant geen token heeft)
    • de server stuurt zijn antwoord. Dit bevat links die de gebruiker moet gebruiken om verder te gaan met de applicatie. In de URL van elk van deze links voegt de server het token toe in de vorm URL;token=waarde.
    • Wanneer de gebruiker op een van de links klikt om de applicatie voort te zetten, doet de browser een verzoek aan de webserver door in de headers HTTP de gevraagde URL URL;token=waarde te verzenden. De server kan dan het token ophalen.

4.2. Java voor sessietracking

We presenteren nu de belangrijkste methoden die nuttig zijn voor sessiebeheer:

HttpSession [HttpServletRequest].getSession()
haalt het object Session op waartoe het huidige verzoek behoort. Als dit verzoek nog geen deel uitmaakte van een sessie, wordt er een sessie aangemaakt.
String [HttpSession].getId()
identificatie van de huidige sessie
long [HttpSession].getCreationTime()
aanmaakdatum van de huidige sessie (aantal milliseconden verstreken sinds 1 januari 1970, 0.00 uur).
long [HttpSession].getLastAccessedTime()
datum van de laatste toegang tot de sessie door de klant
long [HttpSession].getMaxInactiveInterval()
maximale duur in seconden van inactiviteit van een sessie. Na afloop van deze tijd wordt de sessie ongeldig verklaard.
[HttpSession].setMaxInactiveInterval(int durée)
stelt de maximale inactiviteitsduur van een sessie in seconden vast. Na afloop van deze tijd wordt de sessie ongeldig verklaard.
boolean [HttpSession].isNew()
waar als de sessie zojuist is aangemaakt
[HttpSession].setAttribute(String paramètre, Object valeur)
koppelt een waarde aan een parameter in een bepaalde sessie. Dit mechanisme maakt het mogelijk om informatie op te slaan die gedurende de hele sessie beschikbaar blijft.
[HttpSession].removeAttribute(String paramètre)
verwijdert parametre uit de sessiegegevens.
Object [HttpSession].getAttribute(String paramètre)
waarde die is gekoppeld aan de parameter paramètre van de sessie. Geeft null terug als deze niet bestaat.
Enumeration [HttpSession].getAttributeNames()
lijst in de vorm van een opsomming van alle attributen van de huidige sessie
[HttpSession].invalidate()
beëindigt de huidige sessie. Alle informatie die hieraan is gekoppeld, wordt verwijderd.

4.3. Voorbeeld 1

We presenteren een voorbeeld uit het uitstekende boek "Programmeren met J2EE", uitgegeven door Wrox en gedistribueerd door Eyrolles. Dit boek is een schat aan hoogwaardige informatie voor ontwikkelaars van weboplossingen in Java. De toepassing die in dit boek wordt gepresenteerd in de vorm van een enkele Java-servlet, is hier overgenomen in de vorm van een hoofdservlet die gebruikmaakt van JSP-pagina's voor de weergave van de verschillende mogelijke antwoorden aan de klant.

De applicatie heet „sessions“ en is als volgt geconfigureerd in het bestand <tomcat>\conf\server.xml:

                <Context path="/sessions" docBase="e:/data/serge/servlets/sessions" />

In de bovenstaande map docBase bevinden zich de volgende elementen:

Image

De bestanden erreur.jsp, invalide.jsp en valide.jsp zijn alle drie gekoppeld aan de applicatie ‘sessions’. In de bovenstaande map WEB-INF bevinden zich:

Image

Hierboven zien we het configuratiebestand web.xml van de applicatie 'sessions'. In de map classes vinden we het servlet-klassebestand:

Image

Het bestand web.xml van de applicatie is als volgt:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
      <servlet-name>cycledevie</servlet-name>
    <servlet-class>cycledevie</servlet-class>
    <init-param>
          <param-name>urlSessionValide</param-name>
        <param-value>/valide.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlSessionInvalide</param-name>
        <param-value>/invalide.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
      <servlet-name>cycledevie</servlet-name>
    <url-pattern>/cycledevie</url-pattern>
  </servlet-mapping>
</web-app>

De hoofd-servlet heet cycledevie (servlet-name) en is gekoppeld aan het klassebestand cycledevie.class (servlet-class). De servlet heeft een alias /cycledevie (servlet-mapping), waardoor deze via URL en http://localhost:8080/sessions/cycledevie kan worden aangeroepen. De servlet heeft drie initialisatieparameters:

urlSessionValide
URL van de pagina met de kenmerken van de huidige sessie
urlSessionInvalide
URL van de pagina die wordt weergegeven nadat de huidige sessie is beëindigd
urlErreur
URL van de pagina die wordt weergegeven bij een fout bij het initialiseren van de hoofd-servlet cycledevie

De componenten van de applicatie ‘sessions’ zijn de volgende:

cycledevie
hoofdservlet – analyseert het verzoek van de klant:
  • als deze deel uitmaakt van een sessie, wordt de controle overgedragen aan de pagina valide.jsp, die de kenmerken van deze sessie weergeeft. Vanaf deze pagina kan de gebruiker:
    • de pagina opnieuw laden
    • de pagina ongeldig maken
  • als in het verzoek wordt gevraagd om de lopende sessie ongeldig te maken, geeft de servlet het woord door aan de pagina invalide.jsp, die de gebruiker zal voorstellen om een nieuwe sessie aan te maken
  • als de servlet tijdens het opstarten fouten tegenkomt, geeft deze het woord door aan de pagina erreur.jsp, waarop een foutmelding wordt weergegeven.
valide.jsp
  • toont de kenmerken van de huidige sessie en biedt twee links:
    • één om de pagina opnieuw te laden en zo de parameter van de laatste toegang tot de huidige sessie te zien
    • de andere om de huidige sessie te beëindigen
invalide.jsp
wordt weergegeven wanneer de gebruiker de huidige sessie heeft beëindigd. Biedt vervolgens de mogelijkheid om een nieuwe sessie aan te maken.
erreur.jsp
wordt weergegeven wanneer de hoofd-servlet fouten tegenkomt tijdens het opstarten.

De hoofd-servlet cycledevie is als volgt:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class cycledevie extends HttpServlet{

    // instantievariabelen
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // is de initialisatie gelukt?
        if(msgErreur!=null){
             // we geven het over aan de foutpagina
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // de huidige sessie wordt opgehaald
        HttpSession session=request.getSession();

         // de uit te voeren actie wordt geanalyseerd
        String action=request.getParameter("action");
        // de huidige sessie ongeldig maken
        if(action!=null && action.equals("invalider")){
            // de huidige sessie ongeldig maken
            session.invalidate();
             // we geven het stokje door aan de URL urlSessionInvalide
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // andere gevallen
         // het beheer wordt overgedragen aan de URL urlSessionInvalide
        getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // we halen de initialisatieparameters op
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // instellingen ok?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Let op de volgende punten:

  • in de initialisatiemethode haalt de servlet zijn drie parameters op
  • tijdens de verwerking (doGet) van een verzoek doet de servlet het volgende:
    • controleert eerst of er geen fout is opgetreden tijdens de initialisatie. Als dat wel het geval is, geeft de servlet het stokje door aan de pagina erreur.jsp.
    • controleert de waarde van de parameter action. Als deze de waarde "invalider" heeft, geeft de servlet het woord door aan de pagina invalide.jsp, anders aan de pagina valide.jsp.

De pagina JSP valide.jsp voor het weergeven van de kenmerken van de huidige sessie:

<%@ page import="java.util.*" %>

<%
     // jspService
   // hier moeten we de lopende sessie beschrijven
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- begin van de pagina HTML -->
  <html>
      <meta http-equiv="pragma" content="no-cache">
    <head>
        <title>Cycle de vie d'une session</title>
    </head>
    <body>
        <h3>Cycle de vie d'une session</h3>
        <hr>
        <br>Etat session : <%= etat %>
      <br>ID session : <%= session.getId() %>
      <br>Heure de création : <%= new Date(session.getCreationTime()) %>
      <br>Heure du dernier accès : <%= new Date(session.getLastAccessedTime()) %>
      <br>Intervalle maximum d'inactivité : <%= session.getMaxInactiveInterval() %>
      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>
    <body>
  </html>

Merk op dat in de regel

  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";

een sessieobject wordt gebruikt dat uit het niets lijkt te komen. In feite maakt dit object deel uit van de impliciete objecten die ter beschikking staan van de pagina's JSP, net als de objecten request, response, out, config (ServletConfig), context (ServletContext) die we al eerder zijn tegengekomen. De twee links op de pagina verwijzen naar de eerder genoemde servlet cycledevie:

      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>

De link om de sessie te beëindigen bevat de parameter action=invalider, waarmee de servlet cycledevie kan herkennen dat de gebruiker de huidige sessie wil beëindigen. Met de andere link kan de pagina opnieuw worden geladen. Om te voorkomen dat de browser deze pagina uit de cache haalt, wordt de richtlijn HTML gebruikt:

      <meta http-equiv="pragma" content="no-cache">

gebruikt. Deze geeft de browser de instructie om geen cache te gebruiken voor de pagina die hij ontvangt.

De pagina invalide.jsp ziet er als volgt uit:

<!-- begin van de pagina HTML -->
<html>
  <head>
      <title>Cycle de vie d'une session</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session</h3>
      <hr>
    Votre session a été invalidée
    <a href="/sessions/cycledevie">Créer une nouvelle session</a>
  </body>
</html>

Deze bevat een link naar de servlet cycledevie zonder de parameter action. Deze link zorgt ervoor dat de servlet cycledevie een nieuwe sessie aanmaakt.

De pagina erreur.jsp ziet er als volgt uit:

<%
     // jspService
   // hier gaat het om het geval waarin de lopende sessie moet worden beschreven
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- begin van de pagina HTML -->
<html>
  <head>
      <title>Cycle de vie d'une session</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Deze heeft als taak de foutmelding weer te geven die door de servlet cycledevie is doorgegeven. Laten we nu enkele uitvoervoorbeelden bekijken. De servlet wordt een eerste keer aangeroepen:

Image

De bovenstaande pagina geeft aan dat we ons in een nieuwe sessie bevinden. We gebruiken de link „Pagina opnieuw laden”:

Image

Het vorige resultaat geeft aan dat we ons nog steeds in dezelfde sessie bevinden als op de vorige pagina (dezelfde ID). Merk op dat het tijdstip van de laatste toegang tot deze sessie is gewijzigd. Laten we nu de link "Sessie ongeldig maken" gebruiken:

Image

Let op de URL van deze nieuwe pagina met de parameter action=invalider. Laten we de link "Een nieuwe sessie aanmaken" gebruiken om een nieuwe sessie aan te maken:

Image

We zien dat er een nieuwe sessie is gestart. In de voorgaande voorbeelden is de sessie gebaseerd op het mechanisme van cookies. Laten we nu het gebruik van cookies in onze browser uitschakelen en de tests opnieuw uitvoeren. De volgende voorbeelden zijn uitgevoerd met Netscape Communicator. Om een onverklaarbare reden leverden de tests met IE6 onverwachte resultaten op, alsof IE6 cookies bleef gebruiken terwijl deze waren uitgeschakeld. De servlet cycledevie wordt een eerste keer opgevraagd:

Image

We gebruiken nu de link „Pagina opnieuw laden“:

Image

We zien twee dingen:

  • de ID van de sessie is veranderd
  • de servlet herkent de sessie als een nieuwe sessie

De Tomcat-server biedt een oplossing voor het probleem van gebruikers die het gebruik van cookies in hun browser hebben uitgeschakeld. Hij maakt gebruik van twee mechanismen om het token te implementeren waarover we aan het begin van deze paragraaf spraken: cookies en het herschrijven van URL. Als het sessiecookie niet beschikbaar is, zal de server proberen het token te verkrijgen uit de door de klant aangevraagde URL. Hiervoor moet deze het token bevatten. In het algemeen moeten alle links die in een HTML-document naar de webapplicatie worden gegenereerd, het token van de webapplicatie bevatten. Dit kan worden gedaan met de methode encodeURL:

String [HttpResponse].encodeURL(String URL)
voegt het token van de huidige sessie toe aan de als parameter doorgegeven URL in de vorm URL;jsessionid=xxxx

We passen onze applicatie als volgt aan:

  • in de servlet cycledevie.java worden de URL-codes gecodeerd:
             // we geven het stokje door aan de foutpagina
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // we geven het stokje door aan de URL urlSessionInvalide
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // wordt doorgestuurd naar de URL urlSessionInvalide
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • op de pagina valide.jsp zijn de URL-codes gecodeerd:
<%
     // jspService
   // hier gaat het om het beschrijven van de lopende sessie
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
   // codering URL levenscyclus
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • op de pagina invalide.jsp zijn de URL gecodeerd:
<%
     // jspservice – de huidige sessie wordt ongeldig gemaakt
  session.invalidate();
   // codering URL levenscyclus
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

Nu zijn we klaar om te testen. We gebruiken Netscape 4.5 en de cookies zijn uitgeschakeld. We roepen de servlet cycledevie voor de eerste keer op:

Image

en we laden de pagina opnieuw via de link "Pagina opnieuw laden":

Image

We zien dat:

  • de sessie niet is veranderd (nog steeds ID)
  • de URL van de servlet cycledevie bevat inderdaad het token, zoals te zien is in het veld Adresse hierboven
  • de Tomcat-server haalt dus het sessietoken op uit de aangevraagde URL (als de ontwikkelaar ervoor heeft gezorgd dat dit is gecodeerd).

4.4. Voorbeeld 2

We laten nu een voorbeeld zien waarin wordt getoond hoe informatie in de sessie van een klant kan worden opgeslagen. In dit geval is de enige informatie een teller die telkens wordt verhoogd wanneer de gebruiker de URL van de servlet oproept. Wanneer deze voor het eerst wordt opgeroepen, verschijnt de volgende pagina:

Image

Als je op de link "Pagina opnieuw laden" hierboven klikt, krijg je de volgende nieuwe pagina te zien:

Image

De applicatie bestaat uit drie onderdelen:

  • een servlet die het verzoek van de client verwerkt
  • een JSP-pagina die de stand van de teller weergeeft
  • een JSP-pagina die een eventuele fout weergeeft

Deze drie componenten zijn geïnstalleerd in de reeds gebruikte webapplicatie ‘sessions’. Het bijbehorende bestand web.xml is aangepast om de nieuwe servlets te configureren:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
...
  <servlet>
      <servlet-name>compteur</servlet-name>
    <servlet-class>compteur</servlet-class>
    <init-param>
          <param-name>urlAffichageCompteur</param-name>
        <param-value>/compteur.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreurcompteur.jsp</param-value>
    </init-param>
  </servlet>
...
  <servlet-mapping>
      <servlet-name>compteur</servlet-name>
    <url-pattern>/compteur</url-pattern>
  </servlet-mapping>
</web-app>
  • de servlet heet compteur (servlet-name) en is gekoppeld aan het klassebestand compteur.class (servlet-class)
  • de servlet heeft twee initialisatieparameters:
    • urlAffichageCompteur: URL van de pagina JSP waarop de teller wordt weergegeven
    • urlErreur: URL van de pagina JSP voor de weergave van een eventuele fout
  • en een alias /meter waardoor deze wordt aangeroepen via de URL http://localhost:8080/sessions/compteur

De servlet compteur.java is als volgt:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class compteur extends HttpServlet{

     // instantievariabelen
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // is de initialisatie goed verlopen?
        if(msgErreur!=null){
             // we geven het door aan de foutpagina
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // de huidige sessie wordt opgehaald
        HttpSession session=request.getSession();
         // en de teller
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // de teller wordt verhoogd
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // de teller wordt in de sessie opgeslagen
        session.setAttribute("compteur",compteur);
         // en in de verzoek-URL
        request.setAttribute("compteur",compteur);

         // wordt de controle overgedragen aan de URL voor de weergave van de teller
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // de initialisatieparameters worden opgehaald
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

         // instellingen ok?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Deze servlet heeft dezelfde structuur als de servlets die we al eerder hebben gezien. We wijzen alleen even op de afhandeling van de teller:

  • de sessie wordt opgehaald via request.getSession()
  • de teller wordt in deze sessie opgehaald via session.getAttribute("teller")
  • als er een waarde wordt opgehaald via null, betekent dit dat de sessie net is begonnen. De teller wordt dan op 0 gezet.
  • de teller wordt verhoogd, teruggezet in de sessie (session.setAttribute("teller",teller)) en opgenomen in het verzoek dat naar de weergaveservlet wordt gestuurd (request.setAttribute("teller",teller)).

De weergavepagina compteur.jsp ziet er als volgt uit:

<%
     // jspService
   // de teller wordt opgehaald
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- begin van de pagina HTML -->
<html>
  <head>
      <title>Comptage au fil d'une session</title>
  </head>
  <body>
      <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
      <hr>
    compteur = (<%= compteur %>)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

De bovenstaande pagina haalt alleen het attribuut compteur (request.getAttribute("teller")) op dat door de hoofd-servlet is doorgegeven en geeft dit weer.

De foutpagina erreurcompteur.jsp ziet er als volgt uit:

<%
     // jspService
   // er is een fout opgetreden
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- begin van de pagina HTML -->
<html>
  <head>
      <title>Comptage au fil d'une session</title>
  </head>
  <body>
      <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

4.5. Voorbeeld 3

We willen een Java-toepassing schrijven die als client fungeert voor de vorige toepassing compteur. Deze zou de toepassing N keer achter elkaar aanroepen, waarbij N als parameter wordt doorgegeven. Ons doel is om een geprogrammeerde webclient te laten zien en uit te leggen hoe cookies moeten worden beheerd. Ons uitgangspunt is een generieke webclient die wordt beschreven in het Java-handboek van dezelfde auteur. Deze wordt als volgt aangeroepen:

clientweb URL GET/HEAD

  • URL: aangevraagde URL
  • GET/HEAD: GET om de code HTML van de pagina op te vragen, HEAD om alleen de headers op te vragen HTTP

Hier volgt een voorbeeld met de URL en http://localhost:8080/sessions/compteur:


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/compteur GET

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 14:21:18 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=B8A9076E552945009215C34A97A0EC5D;Path=/sessions


<!-- begin van de pagina HTML -->
<html>
  <head>
        <title>Comptage au fil d'une session</title>
  </head>
  <body>
        <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
        <hr>
    compteur = (1)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

Het programma clientweb geeft alles weer wat het van de server ontvangt. Hierboven zien we het commando HTTP Set-cookie waarmee de server een cookie naar zijn client stuurt. In dit geval bevat de cookie twee gegevens:

  • JSESSIONID, het sessietoken
  • Path, dat de URL definieert waartoe de cookie behoort. Path=/sessions geeft de browser aan dat deze het cookie telkens naar de server moet terugsturen wanneer hij een URL opvraagt die begint met /sessions. In de applicatie sessions hebben we verschillende servlets gebruikt, waaronder de servlets /sessions/cycledevie en /sessions/compteur. Als de servlet /sessions/cycledevie wordt aangeroepen, ontvangt de browser een J-token. Als met dezelfde browser vervolgens de servlet /sessions/compteur wordt aangeroepen, stuurt de browser het token J terug naar de server, omdat dit token geldt voor alle URL-servlets die beginnen met /sessions. In ons voorbeeld hoeven de servlets cycledevie en compteur niet hetzelfde sessietoken te delen. Ze hadden dus niet in dezelfde webapplicatie geplaatst mogen worden. Dit is een belangrijk punt om te onthouden: alle servlets van dezelfde applicatie delen hetzelfde sessietoken.
  • Een cookie kan ook een geldigheidsduur hebben. Hier ontbreekt deze informatie. De cookie wordt dus verwijderd zodra de browser wordt gesloten. Een cookie kan bijvoorbeeld een geldigheidsduur van N dagen hebben. Zolang het geldig is, zal de browser het telkens terugsturen wanneer een van de URL-pagina's van zijn domein (Path) wordt bezocht. Laten we een online winkel van CD als voorbeeld nemen. Deze kan het surfgedrag van zijn klant in de catalogus volgen en gaandeweg diens voorkeuren vaststellen: bijvoorbeeld klassieke muziek. Deze voorkeuren kunnen worden opgeslagen in een cookie met een geldigheidsduur van 3 maanden. Als diezelfde klant na een maand terugkeert naar de website, stuurt de browser de cookie terug naar de serverapplicatie. Deze kan dan, op basis van de informatie in de cookie, de gegenereerde pagina’s aanpassen aan de voorkeuren van de klant.

Hieronder volgt de code van de webclient. Deze zal later het uitgangspunt vormen voor een andere client.

// geïmporteerde pakketten
import java.io.*;
import java.net.*;

public class clientweb{

    // vraagt om een URL
     // geeft de inhoud ervan weer op het scherm

    public static void main(String[] args){
        // syntaxis
        final String syntaxe="pg URI GET/HEAD";

        // aantal argumenten
        if(args.length != 2)
            erreur(syntaxe,1);

         // de aangevraagde URI wordt genoteerd
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // controle op de geldigheid van de URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI is onjuist
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // controle van de bestelling
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // onjuiste bestelling
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // de relevante gegevens worden uit URL gehaald
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // we kunnen aan de slag
        Socket  client=null;                        // de klant
        BufferedReader IN=null;                    // de leesstroom van de klant
        PrintWriter OUT=null;                        // de schrijfstroom van de klant
        String réponse=null;                        // antwoord van de server
        try{
             // er wordt verbinding gemaakt met de server
            client=new Socket(host,port);

            // de invoer- en uitvoerstromen van de klant worden aangemaakt TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // URL wordt opgevraagd - verzending van de headers HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // het antwoord wordt gelezen
            while((réponse=IN.readLine())!=null){
                 // het antwoord wordt verwerkt
                System.out.println(réponse);
            }//while
             // het is klaar
            client.close();
        } catch(Exception e){
            // de uitzondering wordt afgehandeld
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // foutmeldingen weergeven
    public static void erreur(String msg, int exitCode){
         // fout weergeven
        System.err.println(msg);
         // afsluiten met fout
        System.exit(exitCode);
    }//fout
}//klasse

We maken nu het programma clientCompteur aan, dat als volgt wordt aangeroepen:

clientCompteur URL N [JSESSIONID]

  • URL: URL van de teller-servlet
  • N: aantal aanroepen dat naar deze servlet moet worden gedaan
  • JSESSIONID: optionele parameter – sessietoken

Het doel van het programma is om de teller-servlet N keer aan te roepen, waarbij de sessiecookie wordt beheerd en telkens de door de server teruggestuurde waarde van de teller wordt weergegeven. Aan het einde van de N aanroepen moet de waarde van de teller N zijn. Hier volgt een eerste uitvoervoorbeeld:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/teller 3
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A;Path=/sessions
cookie trouvÚ : 92DB3808CE8FCB47D47D997C8B52294A

compteur : 1

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 2

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 3

Het programma geeft het volgende weer:

  • de headers HTTP die het naar de server verstuurt in de vorm -->
  • de headers HTTP die het ontvangt
  • de tellerwaarde na elke aanroep

We zien dat bij de eerste aanroep:

  • de client geen cookie verstuurt
  • de server er wel een verstuurt

Bij de volgende verzoeken:

  • stuurt de client systematisch het cookie terug dat hij bij de eerste aanroep van de server heeft ontvangen. Hierdoor kan de server de client herkennen en de teller verhogen.
  • de server stuurt zelf geen cookie meer terug

We starten het vorige programma opnieuw op en geven het bovenstaande token door als derde parameter:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/teller 3 92DB3808CE8FCB47D47D997C8B52294A

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 4

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 5

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 6

Hier zien we dat de server al bij de eerste aanroep van de client een geldige sessiecookie ontvangt. Het is belangrijk te weten dat voor Tomcat de maximale inactiviteitsduur van een sessie standaard 20 minuten is (dit is overigens configureerbaar). Als de tweede aanroep van het programma de bij de eerste aanroep ontvangen cookie snel genoeg verstuurt, beschouwt de server dit als dezelfde sessie. Hier wijzen we op een potentieel beveiligingslek. Als ik in staat ben om een sessietoken op het netwerk te onderscheppen, kan ik me voordoen als degene die de sessie heeft gestart. In ons voorbeeld vertegenwoordigt de eerste aanroep degene die de sessie start (misschien met een gebruikersnaam en wachtwoord die hem het recht geven op een token) en de tweede aanroep degene die het sessietoken van de eerste aanroep heeft ‘gehackt’. Als de lopende transactie een banktransactie is, kan dit zeer vervelende gevolgen hebben...

De clientcode is als volgt:

// geïmporteerde pakketten
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // vraagt om een URL
     // geeft de inhoud ervan weer op het scherm

    public static void main(String[] args){
        // syntaxis
        final String syntaxe="pg URL-COMPTEUR N [JSESSIONID]";

         // aantal argumenten
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // de aangevraagde URL wordt genoteerd
        String URLString=args[0];

        // controle op de geldigheid van de URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI is onjuist
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//opgevangen
         // controle van het aantal oproepen N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // onjuist argument N
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // is het token JSESSIONID als parameter doorgegeven?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // de relevante informatie wordt uit URL gehaald
        String path=url.getPath();
        if(path.equals("")) path="/";
        String query=url.getQuery();
        if(query!=null) query="?"+query; else query="";
        String host=url.getHost();
        int port=url.getPort();
        if(port==-1) port=url.getDefaultPort();

         // we kunnen aan de slag
        Socket  client=null;                        // de client
        BufferedReader IN=null;                    // de leesstroom van de klant
        PrintWriter OUT=null;                        // de schrijfstroom van de klant
        String réponse=null;                        // antwoord van de server
         // het gezochte patroon in de headers HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // het gezochte patroon in de code HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // het resultaat van de vergelijking met het patroon
        Matcher résultat=null;
         // een booleaanse waarde die het resultaat van de zoekopdracht naar de teller weergeeft
        boolean compteurTrouvé;

        try{
             // er worden N verzoeken naar de server verzonden
            for(int i=0;i<N;i++){
                // er wordt verbinding gemaakt met de server
                client=new Socket(host,port);

                // de in- en uitgaande stromen van de klant TCP worden aangemaakt
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // URL wordt opgevraagd – verzending van de headers HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

                 // het antwoord wordt gelezen tot het einde van de headers, waarbij wordt gezocht naar een eventuele cookie
                while((réponse=IN.readLine())!=null){
                     // antwoord volgen
                    System.out.println(réponse);
                     // lege regel?
                    if(réponse.equals("")) break;
                     // regel HTTP is niet leeg
                     // als het sessietoken ontbreekt, wordt ernaar gezocht
                    if (JSESSIONID.equals("")){
                        // de regel HTTP wordt vergeleken met het cookiemodel
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // de cookie is gevonden
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // de HTTP-headers zijn verwerkt – we gaan verder met de code HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // Bevat de huidige regel de teller?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // de teller is gevonden – deze wordt weergegeven
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // het is klaar
                client.close();
            }//for
        } catch(Exception e){
            // de uitzondering wordt afgehandeld
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // foutmeldingen weergeven
    public static void erreur(String msg, int exitCode){
         // fout weergeven
        System.err.println(msg);
         // afsluiten met fout
        System.exit(exitCode);
    }//fout

     // monitoring van client-server-uitwisselingen
    public static void envoie(PrintWriter OUT,String msg){
        // bericht naar de server verzenden
        OUT.println(msg);
         // scherm bijhouden
        System.out.println("--> "+msg);
    }//fout
}//klasse

Laten we de belangrijke punten van dit programma eens onder de loep nemen:

  • er moeten N client-server-uitwisselingen plaatsvinden. Daarom zitten deze in een lus
            for(int i=0;i<N;i++){
  • Bij elke uitwisseling opent de client een verbinding TCP-IP met de server. Zodra deze tot stand is gebracht, stuurt hij de headers HTTP van zijn verzoek naar de server:
                 // URL wordt opgevraagd - verzending van de headers HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

Als het token JSESSIONID beschikbaar is, wordt het in de vorm van een cookie verzonden; anders is het niet beschikbaar.

  • Zodra het verzoek is verzonden, wacht de client op het antwoord van de server. Hij begint met het doorzoeken van de HTTP-headers van dit antwoord op zoek naar een eventueel cookie. Om het te vinden, vergelijkt hij de ontvangen regels met de reguliere expressie van het cookie:
         // het gezochte patroon in de headers HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // het antwoord wordt tot het einde van de headers gelezen, op zoek naar een eventuele cookie
                while((réponse=IN.readLine())!=null){
                     // vervolg van het antwoord
                    System.out.println(réponse);
                     // lege regel?
                    if(réponse.equals("")) break;
                     // regel HTTP is niet leeg
                     // als het sessietoken ontbreekt, wordt ernaar gezocht
                    if (JSESSIONID.equals("")){
                        // de regel HTTP wordt vergeleken met het cookiemodel
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // de cookie is gevonden
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • Zodra het token eenmaal is gevonden, wordt er bij volgende verzoeken aan de server niet meer naar gezocht. Nadat de headers HTTP van het antwoord zijn verwerkt, gaat men verder met de code HTML van datzelfde antwoord. Hierin wordt gezocht naar de regel die de waarde van de teller aangeeft. Ook deze zoekopdracht wordt uitgevoerd met een reguliere expressie:
         // het patroon van de gezochte teller in de code HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // klaar met de headers HTTP – we gaan verder met de code HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // Bevat de huidige regel de teller?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // de teller is gevonden – deze wordt weergegeven
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Voorbeeld 4

In het vorige voorbeeld stuurt de webclient het token terug in de vorm van een cookie. We hebben gezien dat het ook kan worden teruggestuurd in de aangevraagde URL zelf, in de vorm URL;jsessionid=xxx. Laten we dit eens controleren. Het programma clientCompteur.java wordt omgezet in clientCompteur2.java en als volgt gewijzigd:

....
                 // we vragen om de URL - verzending van de headers HTTP
                if(JSESSIONID.equals(""))
                    envoie(OUT,"GET " + path + query + " HTTP/1.1");
                else envoie(OUT,"GET " + path + query + ";jsessionid=" + JSESSIONID + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                envoie(OUT,"Connection: close");
                envoie(OUT,"");
....

De klant vraagt dus de URL van de teller aan via GET URL;jsessionid=xx HTTP/1.1 en verstuurt geen cookie meer. Dit is de enige wijziging. Hieronder staan de resultaten van een eerste verzoek:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/teller 2

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=48A6DBA8357D808EC012AAF3A2AFDA63;Path=/sessions
cookie trouvÚ : 48A6DBA8357D808EC012AAF3A2AFDA63

compteur : 1

--> GET /sessions/compteur;jsessionid=48A6DBA8357D808EC012AAF3A2AFDA63 HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 2

Bij de eerste aanvraag vraagt de client de URL op zonder sessietoken. De server reageert door het token naar de client te sturen. De client vraagt vervolgens dezelfde URL opnieuw op en voegt daarbij het ontvangen token toe. We zien dat de teller inderdaad is verhoogd, wat aantoont dat de server correct heeft herkend dat het om dezelfde sessie ging.

4.7. Voorbeeld 5

Dit voorbeeld toont een applicatie die bestaat uit drie pagina’s, die we page0, page1 en page2 zullen noemen. De gebruiker moet deze in deze volgorde opvragen:

  • pagina0 is een formulier waarin om informatie wordt gevraagd: een naam
  • pagina1 is een formulier dat wordt weergegeven als reactie op het verzenden van het formulier van pagina0. Hierin wordt om een tweede stuk informatie gevraagd: een leeftijd
  • pagina2 is een document HTML dat de via pagina0 verkregen naam en de via pagina1 verkregen leeftijd weergeeft.

Er vinden hier drie client-server-uitwisselingen plaats:

  • bij de eerste uitwisseling wordt het formulier van pagina0 door de klant opgevraagd en door de server verzonden
  • bij de tweede uitwisseling wordt het formulier page1 door de client opgevraagd en door de server verzonden. De client stuurt de naam naar de server.
  • bij de derde uitwisseling wordt het document page3 door de client opgevraagd en door de server verzonden. De client stuurt de leeftijd naar de server. Het document page3 moet de naam en de leeftijd weergeven. De naam is bij de tweede uitwisseling door de server ontvangen en is sindsdien ‘vergeten’. Er wordt een sessie gebruikt om de naam bij uitwisseling 2 op te slaan, zodat deze bij uitwisseling 3 beschikbaar is.

De pagina page0 die bij de eerste uitwisseling is verkregen, ziet er als volgt uit:

Image

We vullen het veld voor de naam in:

Image

We gebruiken de knop Suite en krijgen dan de volgende pagina page1:

Image

We vullen het veld voor de leeftijd in:

Image

We klikken op de knop Suite en krijgen dan de volgende pagina page2 te zien:

Image

Wanneer we pagina page0 naar de server verzenden, kan deze de pagina met een foutcode terugsturen als de naam leeg is:

Image

Wanneer we de pagina page1 naar de server verzenden, kan deze de pagina met een foutcode terugsturen als de leeftijd ongeldig is:

Image

De applicatie bestaat uit een servlet en vier pagina’s JSP:

page0.jsp
geeft pagina0 weer
page1.jsp
toont pagina 1
page2.jsp
toont pagina 2
erreur.jsp
toont een foutpagina

De webapplicatie heet suitedepages en is als volgt geconfigureerd in het Tomcat-bestand server.xml:

                <Context path="/suitedepages" docBase="e:/data/serge/servlets/suitedepages" />

Het configuratiebestand web.xml van de toepassing suitedepages ziet er als volgt uit:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
      <servlet-name>main</servlet-name>
    <servlet-class>main</servlet-class>
    <init-param>
          <param-name>urlPage0</param-name>
        <param-value>/page0.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlPage1</param-name>
        <param-value>/page1.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlPage2</param-name>
        <param-value>/page2.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>    
  </servlet>
  <servlet-mapping>
      <servlet-name>main</servlet-name>
    <url-pattern>/main</url-pattern>
  </servlet-mapping>
</web-app>

De hoofd-servlet heet 'main' en is dankzij de alias (servlet-mapping) bereikbaar via de URL http://localhost:8080/suitedepages/main. Deze heeft vier initialisatieparameters, namelijk de URL van de vier pagina’s JSP die voor de verschillende weergaven worden gebruikt. De code van de hoofd-servlet is als volgt:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.util.regex.*;

public class main extends HttpServlet{

    // instantievariabelen
    String msgErreur=null;
    String urlPage0=null;
    String urlPage1=null;
    String urlPage2=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // is de initialisatie goed verlopen?
        if(msgErreur!=null){
             // we geven het door aan de foutpagina
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // de stap-parameter wordt opgehaald
        String étape=request.getParameter("etape");
         // de huidige sessie wordt opgehaald
        HttpSession session=request.getSession();
         // de huidige stap wordt verwerkt
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // andere gevallen zijn ongeldig
        étape0(request,response,session);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // de initialisatieparameters worden opgehaald
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

         // parameters ok?
        if(urlPage0==null || urlPage1==null || urlPage2==null){
            msgErreur="Configuration incorrecte";
        }
    }

     //-------- stap 0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // we stellen enkele attributen in
        request.setAttribute("nom","");
         // pagina 0 wordt weergegeven
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- stap 1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // we halen de naam op uit de query
        String nom=request.getParameter("nom");
        // naam ingesteld?
        if(nom==null) étape0(request,response,session);
         // eventuele spaties uit de naam verwijderen
        nom=nom.trim();
         // de naam wordt in een attribuut van de query geplaatst
        request.setAttribute("nom",nom);
         // is de naam leeg?
        if(nom.equals("")){
             // dit is een fout
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // de fouten worden in de query opgenomen
            request.setAttribute("erreurs",erreurs);
             // terug naar pagina 0
            étape0(request,response,session);
        }
         // geldige naam  deze wordt opgeslagen in de huidige sessie
        session.setAttribute("nom",nom);
         // het attribuut leeftijd wordt in de query vastgelegd
        request.setAttribute("age","");
         // pagina 1 wordt weergegeven
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- stap 2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // de naam wordt opgehaald uit de sessie
        String nom=(String)session.getAttribute("nom");
         // is de naam ingesteld?
        if(nom==null) étape0(request,response,session);
         // we plaatsen deze in een attribuut van de query
        request.setAttribute("nom",nom);
         // de leeftijd wordt opgehaald uit de query
        String age=request.getParameter("age");
        // leeftijd ingesteld?
        if(age==null){
            // terug naar pagina 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // de leeftijd wordt opgeslagen in de query
        age=age.trim();
        request.setAttribute("age",age);
        // is de leeftijd geldig?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // dit is een fout
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // de fouten worden in de query opgenomen
            request.setAttribute("erreurs",erreurs);
             // terug naar pagina 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // geldige leeftijd  pagina 2 wordt weergegeven
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • De methode init haalt de vier initialisatieparameters op en genereert een foutmelding als er een ontbreekt
  • we hebben gezien dat de aanvraag uit drie uitwisselingen bestond. Om te weten hoe ver we hierin zijn, bevatten de formulieren page0 en page1 een verborgen variabele etape die de waarde 1 (page0) of 2 (page1). Dit nummer kan hier worden gezien als het nummer van de volgende pagina die moet worden weergegeven. In de methode doGet wordt deze parameter uit de verzoek-URL opgehaald en afhankelijk van de waarde ervan wordt de verwerking doorgestuurd naar drie andere methoden:
    • étape0 verwerkt het oorspronkelijke verzoek en roept page0 aan
    • étape1 verwerkt het formulier van page0 en stuurt page1 of opnieuw page0 als er een fout is opgetreden
    • stap2 verwerkt het formulier page1 en verstuurt page2 of opnieuw page1 als er een fout is opgetreden
  • stap 0
    • geeft page0 weer met een lege naam
  • stap1
    • haalt de parameter nom op uit het formulier page0.
    • controleert of de naam bestaat (niet null). Als dat niet het geval is, wordt page0 opnieuw weergegeven alsof het de eerste aanroep was.
    • Controleert of de naam niet leeg is. Als dat niet het geval is, wordt page0 opnieuw weergegeven met een foutmelding.
    • slaat de naam op in de huidige sessie en geeft page1 weer als de naam geldig is.
  • stap 2
    • haalt de parameter nom op uit de huidige sessie.
    • Controleert of de naam bestaat (niet null). Als dat niet het geval is, wordt opnieuw page0 weergegeven alsof het de eerste aanroep was.
    • haalt de parameter age op uit de huidige aanvraag die is verzonden door page1.
    • controleert of de leeftijd geldig is. Als dat niet het geval is, wordt page1 opnieuw weergegeven met een foutmelding.
    • slaat de naam en de leeftijd op als verzoekattributen en geeft page2 weer als de naam en de leeftijd geldig zijn.

De pagina page0.jsp ziet er als volgt uit:

<%@ page import="java.util.*" %>

<% // page0.jsp
     // de attributen van de aanvraag worden opgehaald
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
   // zijn de attributen geldig?
  if(nom==null){
       // terug naar de hoofd-servlet
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 0</title>
  </head>
  <body>
    <h3>Page 0/2</h3>
    <form name="frmNom" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="1">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<%= nom %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // fouten?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>
  • De pagina page0.jsp kan in twee gevallen door de hoofd-servlet worden aangeroepen:
    • bij de eerste verzoek
    • na verwerking van het formulier van page0 wanneer er een fout is opgetreden
  • De weer te geven parameter nom wordt door de hoofd-servlet doorgegeven, evenals de eventuele lijst met fouten. De servlet page0.jsp begint dus met het ophalen van deze twee gegevens.
  • Het formulier wordt naar de hoofd-servlet ‘verzonden’ met het verborgen veld (hidden) etape, dat aangeeft in welke fase van de applicatie men zich bevindt.

De pagina page1.jsp ziet er als volgt uit:

<%@ page import="java.util.*" %>

<% // page1.jsp
     // de attributen van het verzoek worden opgehaald
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // zijn de attributen geldig?
  if(nom==null || age==null){
      // terug naar de hoofd-servlet
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/2</h3>
    <form name="frmAge" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="2">    
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<%= age %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // fouten?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>

De pagina page1.jsp heeft een structuur die vergelijkbaar is met die van de pagina page0.jsp, met dit verschil dat deze nu twee attributen van de hoofd-servlet ontvangt: nom en age. Ten slotte ziet de pagina page2.jsp er als volgt uit:

<% 
     // page2.jsp
     // de attributen van het verzoek worden opgehaald
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // zijn de attributen geldig?
  if(nom==null || age==null){
      // terug naar de hoofd-servlet
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  


<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/2</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><%= age %></font></td>
        </tr>
      </table>
  </body>
</html>

Ook de pagina page2.jsp ontvangt de attributen nom en age van de hoofd-servlet. Deze pagina geeft ze alleen weer. Tot slot is de pagina erreur.jsp, die bedoeld is om een foutmelding weer te geven in geval van een onjuiste initialisatie van de servlet, als volgt:

<%
     // jspService
   // er is een fout opgetreden
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- begin van de pagina HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Het geeft het attribuut msgErreur weer dat door de hoofd-servlet is doorgegeven.

Concluderend valt op dat tijdens de drie fasen van de applicatie de browser altijd eerst de hoofd-servlet opvraagt. Maar het is niet de hoofd-servlet die het weer te geven antwoord genereert, maar een van de vier pagina’s JSP. De gebruiker merkt dit niet op, omdat de browser in het adresveld de oorspronkelijk opgevraagde URL blijft weergeven, dus die van de hoofd-servlet.