-+  Associazione
-+  Documenti
 |-  Modern Perl
 |-  Bibliografia
 |-  Articoli
 |-  Talk
 |-  Perlfunc
 |-  F.A.Q.
 |-  F.A.Q. iclp
-+  Eventi
-+  Community
-+  Blog
-+  Link
Corso di Perl



 

Versione stampabile.

Enrico Sorcinelli
Continuiamo il nostro viaggio all'interno della tecnologia Apache/mod_perl. In questa puntata vedremo come gestire le sessioni in mod_perl
Parte IV

Data di pubblicazione: Linux & C., anno 4, numero 25 (Giugno 2002).

©2003 Linux & C. Tutti i diritti riservati. E' vietata la copia e la riproduzione o ridistribuzione dell'articolo senza l'espresso permesso scritto dell'autore e dell'editore.

1. Introduzione 

2. HTTP: un protocollo senza memoria 

3. Apache::Session 

4. La gestione delle sessioni 

5. Memorizzare le sessioni in un database 

6. Conclusioni 

7. Appendice A  

8. Convenzioni usate in questo articolo 

Introduzione

Le moderne applicazioni web richiedono sempre più una complessa interazione con l'utente. Un tipico esempio (molto citato) è il 'carrello della spesa' dove, durante la navigazione del catalogo, i prodotti inseriti nel carrello devono venire memorizzati da qualche parte per essere poi recuperati all'atto dell'acquisto. Questo significa ricordarsi ogni volta delle scelte fatte dall'utente nelle pagine visitate e rendere disponibili queste informazioni per le richieste future. In questo articolo affronteremo questa problematica in ottica mod_perl tramite la gestione delle sessioni.

 HTTP: un protocollo senza memoria

Uno dei principali inconvenienti nello sviluppo di applicazioni web è che l'architettura del protocollo HTTP è stateless cioè senza memoria di stato. Ad ogni richiesta di un oggetto (documento HTML, immagine, ecc.) il browser deve:

  • aprire una connessione verso il server
  • scaricare il documento
  • chiudere la connessione

Questo ciclo è assai costoso per pagine che contengono ad esempio molte immagini: ogni URL è gestita infatti da una richiesta separata.

Le modifiche apportate al protocollo HTTP con la versione 1.1 cercano di risolvere in parte questo problema. Infatti una connessione può essere mantenuta per un certo periodo di tempo invece che essere subito chiusa e le richieste provenienti da uno stesso browser possono riusare quella connessione invece di crearne una nuova.

Tuttavia, una volta terminata la transazione HTTP, il server 'dimentica' tutto senza memorizzare informazione alcuna per le transazioni successive né recuperarne da quelle precedenti (Figura 1). Questo rende assai ardua la realizzazione di web-application che richiedono il mantenimento dello stato come applicazioni di e-commerce (shopping cart), questionari multipli, wizard, pagine personalizzate, ecc.

Figura 1. Applicazione senza stato (stateless)

Applicazione senza stato - stateless

La natura stateless del protocollo HTTP è stata in un certo senso uno stimolo per i Web developer a cercare soluzioni per risolvere il problema del mantenimento dello stato attraverso le richieste HTTP: nasce così il concetto di sessione. In breve la sessione definisce il contesto utente (cioè le variabili di stato, o di sessione appunto, associate a quell'utente) nel quale la richiesta deve essere processata.

Le tecniche di gestione delle sessioni possono essere suddivise in due gruppi principali: tecniche client-side e tecniche server-side.

Per tecniche client-side intendiamo che le informazioni dello stato utente vengono mantenute nel browser con le seguenti modalità:

  • nei cookies
  • in campi nascosti ('hidden') di una FORM
  • come parte della URI

Queste tecniche hanno l'indubbio vantaggio di essere semplici da realizzare ed evitano la necessità di dovere memorizzare sul server le informazioni di sessione, cosa che può rivelarsi tediosa, oltreché complessa. Tuttavia ci sono delle controindicazioni. Nel caso si utilizzino i cookies infatti, abbiamo il problema della privacy per cui l'utente potrebbe disabilitarne l'uso. Se poi le informazioni sono tante, c'è il vincolo della lunghezza massima di 4 KB per ogni cookie e un massimo di 20 cookies per dominio (quindi 80 KB di spazio massimo per sessione). Nel secondo caso (campi 'hidden') l'utente è libero di analizzare le informazioni che sono scritte nel sorgente HTML della pagina, e modificarle per tentare un submit 'improprio' della form.

In tutti i casi comunque è evidente che se le informazioni relative allo stato dovessero essere numerose si potrebbe incorrere in problematiche di consumo eccessivo di banda dal momento che ad ogni richiesta questi dati vanno e vengono dal client al server e viceversa.

Per tecniche server-side si intende che le informazioni dello stato, durante le richieste HTTP, vengono mantenute e salvate sul server in maniera trasparente all'utente. Le informazioni possono essere memorizzate in:

  • Memoria
  • File system
  • Database

Tipicamente questo avviene creando sul server un oggetto 'sessione' che contiene tutte le informazioni relative alla stato dell'utente. Ad ogni sessione viene univocamente associata una chiave e questa è la sola informazione che dovremo preoccuparci di inviare al browser utilizzando uno dei metodi client-side illustrati.

Memorizzare lo stato a livello di web server risolve quindi il problema della dimensione della richiesta HTTP e protegge le informazioni dello stato da accidentali o intenzionali modifiche che l'utente può fare. Per ogni richiesta, l'applicazione deve recuperare la chiave dal browser e ristabilire la relativa sessione ripristinando lo stato associato all'utente. Terminata la richiesta, l'applicazione deve infine preoccuparsi di aggiornare le informazioni di quella sessione nel supporto di memorizzazione utilizzato (Figura 2).

Figura 2. Applicazione con mantenimento dello stato

(stateful)

Applicazione con 
mantenimento dello stato - stateful

 Apache::Session

Apache::Session è un modulo Perl che fornisce un framework di base per la memorizzazione persistente di dati arbitrari (scalari, array, oggetti, ecc.) su differenti meccanismi di memorizzazione quali ad esempio RAM, file system o database. In questo paragrafo illustreremo come utilizzare la classe Apache::Session::File per gestire le sessioni in mod_perl e salvare i dati della sessione utente lato server (su file system) e useremo i cookies per inviare e recuperare dal browser la chiave associata alla sessione. Unico prerequisito per Apache::Session è il modulo Digest::MD5 utilizzato per la generazione delle chiavi delle sessioni. Entrambi i moduli sono come sempre disponibili su CPAN (http://www.cpan.org) e si installano nella maniera tradizionale:

   %> perl Makefile.PL 
   %> make 
   %> make test 
   %> make install

nonché, se siamo collegati in rete, direttamente dal CPAN:

   %> perl -MCPAN -e "install 'Apache::Session'"

Apache::Session utilizza la funzione Perl tie per creare un legame tra l'hash (%session) contenente i dati dello stato e la classe. In questo modo il programmatore semplicemente interagisce con l'hash legato: tutti i dettagli dell'interazione con il supporto di memorizzazione (cioè lettura e salvataggio) sono impliciti dentro la specifica implementazione della classe Apache::Session e noi non dovremo preoccuparcene.

Apache::Session utilizza l'algoritmo MD5 per la generazione di una chiave (ID) univoca lunga 32 caratteri da associare alla sessione. Ad esempio:

   f64621aeaa43778neafa43b05f6d3858

 La gestione delle sessioni

Se Apache::Session automatizza tutte le operazioni di memorizzazione e recupero delle informazioni di stato e di generazione di una chiave, non ci aiuta molto nel setup e tracking di una sessione. La soluzione che adottiamo prevede la realizzazione di un semplice modulo mod_perl che, utilizzando Apache::Session, automatizzi le seguenti azioni:

  • recuperare l'ID di sessione dal cookie del browser e ripristinare la sessione;
  • generare e assegnare un nuovo ID di sessione per tutti gli utenti (browser) che non ne abbiano;
  • rendere disponibile l'oggetto sessione a tutti gli altri moduli che implementino una fase (in particolare la Response Phase) della transazione HTTP;
  • implementare un semplice meccanismo di garbage collection per le sessioni scadute.

Per raggiungere in maniera elegante il nostro obiettivo sfrutteremo la potenza del mod_perl. Tramite il meccanismo della ridefinizione degli handler della Request Loop di Apache (che abbiamo visto nel n 23) installeremo il nostro modulo nella fase di Header Parsing, con la direttiva PerlHeaderParserHandler. Ciò renderà trasparente la gestione delle sessioni: infatti il nostro modulo creerà un oggetto sessione per noi e lo renderà disponibile a tutti gli altri handler con il meccanismo delle pnotes(). L'Esempio 1 illustra la nostra semplice implementazione di quanto detto fino ad ora.

Letti i valori delle direttive in httpd.conf tramite le chiamate a $r->dir_config(), recuperiamo l'ID di sessione nel cookie inviato dal browser. Notiamo che si utilizzano i metodi che mod_perl ci mette a disposizione per la gestione dei cookies tramite l'utilizzo della classe Apache::Cookie:

   my %cookies = Apache::Cookie->fetch; 
   $cookies{'cookie_name'}->value;

per la lettura dei cookies e:

   my $cookie = new Apache::Cookie ($r, %args );
   $cookie->bake;

per settare un cookie al browser.

Una nuova sessione viene creata con:

   tie %session, 'Apache::Session::File', $cookie_id, {
         Directory => $session_config{'SessionDirectory'},
         LockDirectory  => $session_config{'SessionLockDirectory'}
      };

questo ci consente direttamente di salvare e leggere informazioni dalla sessione corrente semplicemente accedendo all'hash legato %session (in realtà ci pensa poi la classe Apache::Session::File a leggere e a scrivere su file, ma questo avviene in maniera a noi trasparente!).

Il meccanismo con il quale rendiamo disponibile l'hash %session a tutte le successive fasi della richiesta HTTP è:

	
   $r->pnotes('SESSION_HANDLE' => \%session );

Il metodo pnotes() ci mette a disposizione un potente strumento per memorizzare un qualsiasi scalare Perl e renderlo disponibile a tutti gli altri handler: per strutture dati complesse passiamo un riferimento ad esse. Non ci dovremo infine preoccupare di deallocare tutte le variabili che abbiamo definito con pnotes() poiché alla fine di ogni richiesta mod_perl farà questo per noi.

Esempio 1. Apache/SessionManager.pm

   package Apache::SessionManager;

   use strict;
   use Apache::Constants qw(:common);
   use Apache::Cookie ();
   use Apache::Session::File;   

   sub setup {
      my $r = shift;
      my %session_config;
      $session_config{'SessionExpire'} = $r->dir_config("SessionExpire") || 60;
      $session_config{'SessionName'} = $r->dir_config("SessionName") || 'PERLSESSIONID'; 	
      $session_config{'SessionDirectory'} = $r->dir_config("SessionDirectory") || '/tmp';
      $session_config{'SessionLockDirectory'} = $r->dir_config("SessionLockDirectory") || '/tmp';

      # Fetch cookies
      my %cookies = Apache::Cookie->fetch; 
      my $cookie_id = $cookies{$session_config{'SessionName'}}->value 
         if defined $cookies{$session_config{'SessionName'}};
      my %session;

      # recupera l'oggetto sessione a partire dall'ID del cookie di sessione
      eval {
         tie %session, 'Apache::Session::File', $cookie_id, {
               Directory     => $session_config{'SessionDirectory'},
               LockDirectory => $session_config{'SessionLockDirectory'}
            };
      };
      # Cookie non presente/sessione non valida: crea un nuovo oggetto sessione  
      if ($@) {
         tie %session, 'Apache::Session::File', undef, {
               Directory     => $session_config{'SessionDirectory'},
               LockDirectory => $session_config{'SessionLockDirectory'}
            };       
         $cookie_id = undef;
         $session{'start'} = time;
      }
      # check dell'expiration sessione (garbage collection)
      else {
         # Sessione scaduta: viene creato un nuovo oggetto sessione
         if ( (time - $session{'start'}) > $session_config{'SessionExpire'} ) {
            tied(%session)->delete;
            tie %session, 'Apache::Session::File', undef, { 
                  Directory     => $session_config{'SessionDirectory'},
                  LockDirectory => $session_config{'SessionLockDirectory'}
                };       
            $cookie_id = undef;
            $session{'start'} = time;
         }
      }
      # per ogni nuova sessione, inviamo il cookie al browser 
      unless ( $cookie_id ) {
         my $cookie = Apache::Cookie->new($r,
            -name  => $session_config{'SessionName'},
            -value => $session{_session_id},
            -path  => '/'
         );
         $cookie->bake;
      }
      # memorizza il riferimento all'oggetto sessione per gli altri handler della request
      $r->pnotes('SESSION_HANDLE' => \%session );
      # registra ed esegue la sub cleanup alla fine della request
      $r->register_cleanup(\&cleanup);
      return DECLINED;
   }

   sub cleanup {
      my $r = shift;
      my $session = ref $r->pnotes('SESSION_HANDLE') ? $r->pnotes('SESSION_HANDLE') : {};
      untie %{$session};
      return DECLINED;
   }

   1;
   __END__ 

Salviamo il modulo su file system (ad es. in /home/web/Apache/SessionManager.pm) e configuriamo in httpd.conf con il seguente blocco di direttive:

   <IfModule mod_perl.c>
      <Perl>
         use lib '/home/web';
      </Perl>
      PerlModule Apache::SessionManager
      PerlModule Apache::SessionTest
      <Location /test-session>
         SetHandler perl-script
         PerlSetVar SessionExpire 60
         PerlSetVar SessionName PERLSESSIONID
         PerlSetVar SessionDirectory "/tmp/apache_session_data"
         PerlSetVar SessionLockDirectory "/tmp/apache_session_data/lock"
         PerlHeaderParserHandler Apache::SessionManager
         PerlHandler Apache::SessionTest
      </Location>
   </IfModule>

Commentiamo brevemente la configurazione. Dopo aver aggiunto la directory home/lib ai path presenti in @INC, precarichiamo i nostri moduli Apache::SessionManager e Apache::SessionTest (che vedremo più avanti).

Con le varie direttive PerlSetVar abbiamo la possibilità di configurare il comportamento del modulo di gestione delle sessioni. In particolare SessioneExpire definisce la durata massima di una sessione (in secondi), SessioneName definisce il nome del cookie di sessione inviato al browser, SessionDirectory e SessionLockDirectory specificano le directory su file system usate da Apache::Session::File per la memorizzazione delle informazioni relative alle sessioni. Assicuriamoci dunque che l'utente Apache abbia permessi di lettura e scrittura in queste directory!

Come semplice esempio di applicazione del nostro modulo, scriviamo un altro modulo Apache/SessionTest.pm (Esempio 2), responsabile della Response Phase per la generazione dei contenuti che non fa altro che fotografare lo stato corrente stampando le variabili di sessione istanziate. Possiamo poi, con il submit della semplice form, definire e salvare nella sessione corrente nuove variabili di stato (Figura 3).

Figura 3. Output del modulo Apache/SessionTest.pm

Output del modulo Apache/SessionTest.pm

L'istruzione tramite la quale recuperiamo l'oggetto sessione (e quindi tutte le informazioni salvate nello stato durante le successive transazioni HTTP) è:

   my $session = ref $r->pnotes('SESSION_HANDLE') 
      ? $r->pnotes('SESSION_HANDLE') : {};

che recupera con pnotes() il riferimento ad un oggetto sessione istanziato dal modulo Apache::SessionManager nella fase di Header Parsing della richiesta HTTP. Da questo momento in poi possiamo leggere le variabili di sessione salvate nelle precedenti transazioni HTTP direttamente con:

   print $$session{'old_session_var'};
   # equivalente a
   print $session->{'old_session_var'};

e crearne di nuove (o modificarle) con:

   $$session{'new_session_var'} = 'bla bla';
   # equivalente a
   $session->{'new_session_var'} = 'bla bla';

esattamente come se lavorassimo con un hash (un riferimento in questo caso). Per provare il modulo, riavviamo Apache e lanciamo la URL: http://localhost/test-session (relativa alla URI che abbiamo definito con la direttiva <Location>). Provate a cambiare e aggiungere variabili di sessione con l'apposita form e noterete che le informazioni visualizzate cambieranno di conseguenza. Allo scadere poi della sessione tutte le variabili precedentemente impostate non saranno stampate in quanto non più disponibili.

Esempio 2. Apache/SessionTest.pm

   package Apache::SessionTest;

   use strict;
   use Apache::Constants qw(:common);
   use Apache::Request ();

   sub handler {
      my $r = shift;
      my $str;

      # Recupera la sessione
      my $session = ref $r->pnotes('SESSION_HANDLE') ? $r->pnotes('SESSION_HANDLE') : {};
      $$session{'timestamp'} = time;

      # Recupera i parametri della FORM e li assegna alla sessione
      my $form = new Apache::Request($r);
      $$session{$form->param('name')} = $form->param('value')
         if ( $form->param('name') && $form->param('value') ); 

      $str = <<EOM;
   <HTML>
   <HEAD><TITLE>Session Test</TITLE></HEAD>
   <BODY>
   <CENTER><H1>Session management with cookies</H1>
   <FORM METHOD="POST">
   Nuova variabile di sessione: <INPUT TYPE="text" NAME="name" SIZE="10">
   Valore: <INPUT TYPE="text" NAME="value" SIZE="10">
   <INPUT TYPE="submit" VALUE="Invia">
   </FORM>
   <TABLE BORDER="1">
    <TR ALIGN="center"><TD COLSPAN="2"><B>Variabili di sessione</B></TD></TR>
   EOM
      foreach (sort keys %{$session}) {
         $str .= " <TR><TD>$_</TD><TD>$$session{$_}</TD></TR>\n";
      }
      $str .= "</TABLE></CENTER>\n</BODY><HTML>";

      # Output HTML al client
      $r->content_type('text/html'); 
      $r->send_http_header;
      $r->print($str);
      return OK;
   }

   1;
   __END__

 Memorizzare le sessioni in un database

La soluzione di scegliere il file system come datastore è poco scalabile ed è più o meno vincolante all'utilizzo di una sola macchina (a meno di non utilizzare file system distribuiti come NFS): in situazioni di load-balancing avremmo poi difficoltà a garantire la consistenza degli ID di sessione.

Per risolvere questo problema, accenniamo brevemente a come modificare il modulo Apache::SessionManager per memorizzare le informazioni relative alle sessioni in un database. Nel nostro esempio faremo riferimento a MySQL.

Una delle caratteristiche più interessanti di Apache::Session è la sua architettura ad oggetti che lo rende indipendente dal supporto di memorizzazione: di fatto è possibile subclassare la classe scrivendo veri e propri plug in per differenti datastore. Attualmente è possibile principalmente archiviare sessioni su file, in RAM e su database (vedere L). Nel nostro caso questo plug in si chiama Apache::Session::MySQL. Oltre a sostituire la linea:

   use Apache::Session::File;   

con:

   use Apache::Session::MySQL;

vanno modificate le parti di codice relative alle chiamate della funzione tie:

   tie %session, 'Apache::Session::MySQL', $cookie_id, {
         DataSource => 'dbi:mysql:sessions',
         UserName   => $db_user,
         Password   => $db_pass,
         LockDataSource => 'dbi:mysql:sessions',
         LockUserName   => $db_user,
         LockPassword   => $db_pass
      };

È altresì possibile utilizzare connessioni già aperte specificando come opzione il relativo handle DBI:

   tie %session, 'Apache::Session::MySQL', $cookie_id, {
         Handle => $dbh,
         LockHandle => $dbh
     };

Va ricordato infine che Apache::Session::MySQL non crea automaticamente lo schema del database, per cui dobbiamo creare noi la tabella utilizzata per archiviare le informazioni relative alle sessioni. Supponendo di avere una shell mysql a portata di mano, ecco i comandi per la creazione dello schema:

   mysql> CREATE DATABASE sessions;
   Query OK, 0 rows affected (0.01 sec)
   mysql> CREATE TABLE sessions (
        > id char(32) not null primary key,
        > a_session text );
   Query OK, 0 rows affected (0.01 sec)

 Conclusioni

In questo articolo abbiamo analizzato la gestione delle sessioni in mod_perl e proposto una semplice soluzione. Il lettore può infatti ampliare il modulo Apache::SessionManager per aggiungere caratteristiche avanzate (facendo scadere ad es. le sessioni dopo un tempo di inattività piuttosto che dopo un tempo di vita fisso), così come è relativamente semplice modificare il codice, come abbiamo visto, per far sì che le sessioni vengano mantenute in un RBDMS utilizzando di volta in volta le opportune classi (vedere L).

Gran parte dei concetti illustrati in questo articolo sono alla base del modulo Apache::SessionManager disponibile su CPAN (http://search.cpan.org/~enrys/Apache-SessionManager-1.00)

Nella prossima puntata concluderemo la serie sul mod_perl analizzando i più diffusi framework opensource basati su mod_perl.

 Appendice A

Questa appendice illustra le principali classi Apache::Session::* disponibili per differenti datastore.

Le seguenti implementazioni sono incluse nella distribuzione di Apache::Session:

Le seguenti implementazioni sono disponibili come moduli aggiuntivi su CPAN:

  • Apache::Session::SQLite
    Utilizza il database SQLite per la memorizzazione delle sessioni
  • Apache::Session::SharedMem
    Utilizza la RAM (shared memory) per la memorizzazione delle sessioni (richiede

    IPC::Cache)

  • Apache::Session::PHP
    Permette di condividere le sessioni con le applicazioni PHP utilizzando il

    formato del PHP4 per la lettura e scrittura delle sessioni su file

  • Apache::Session::CacheAny
    Questo modulo consente di utilizzare le classi Cache::Cache come implementazione

    per lo storage di Apache::Session.

 Convenzioni usate in questo articolo

Le seguenti convenzioni tipografiche sono stato utilizzate in questo articolo:

Corsivo

Utilizzato nelle URL, nei percorsi di file, nelle pagine di manuale, nei titoli di libri e/o articoli. Anche nuovi termini cui si intende dare particolare rilievo sono formattati in corsivo.

Larghezza costante

Utilizzato negli esempi di codice e file di configurazione, nel codice che appare all'interno del testo e negli esempi che illustrano cosa digitare a linea di comando.

Negli articoli ci sono molti esempi di codice Perl: alcuni sono solamente pezzi di codice, altri invece programmi completi che possono essere individuati poiché iniziano tutti con la linea #! (shebang):

   #!/usr/bin/perl

Tutti gli esempi che illustrano procedure a linea di comando usano %> per indicare il generico prompt della shell:

   %> perl -e 'print "Hello world\n"'
      Hello word

Occasionalmente viene utilizzato #> per indicare espressamente la shell dell'utente root:

   #> perl -e 'print "Hello world\n"'
      Hello word

D:
Progetti e documenti in rilievo
Corso di Perl Progetto pod2it
D:
La ML di Perl.it
mongers@perl.it è la lista ufficiale di Perl Mongers Italia per porre quesiti di tipo tecnico, per rimanere aggiornato su meeting, incontri, manifestazioni e novità su Perl.it.
Iscriviti!
D:
Annunci Google