Salta al contenuto
Note per Studenti Esercizio - Ciclo di vita, concorrenza, sessioni e filtri di una servlet (applicazione web)

Esercizio - Ciclo di vita, concorrenza, sessioni e filtri di una servlet (applicazione web)

Esercizio originale, nello stile delle domande teoriche e di lettura del codice dell'esame. Teoria: Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioniUna servlet è una classe Java che un container (Apache Tomcat: connettore HTTP Coyote, contenitore Catalina, compilatore JSP Jasper) istanzia una sola volta e chiama per ogni richiesta, ciascuna in un thread del pool. Ciclo di vita: caricamento e costruzione, init() una volta, service() per ogni richiesta (HttpServlet lo smista su doGet, doPost, doPut, doDelete), destroy() allo spegnimento. La richiesta (HttpServletRequest) dà metodo, parametri (query string e corpo del form), header, path (contextPath, servletPath, pathInfo) e corpo; la risposta (HttpServletResponse) si costruisce con stato, header e corpo (PrintWriter), con sendRedirect (302 + Location) o sendError. Il mapping si dichiara con @WebServlet o in WEB-INF/web.xml (che prevale). Poiché esiste una sola istanza e molti thread, i campi di istanza sono condivisi: lo stato della richiesta sta in variabili locali, quello condiviso in oggetti thread-safe (AtomicInteger, DataSource). HTTP è senza stato, quindi si usano le sessioni: la servlet crea un HttpSession, tiene i dati sul server e manda al browser solo l'identificatore nel cookie JSESSIONID (Set-Cookie con HttpOnly, Secure, SameSite, Path secondo RFC 6265); al login si cambia l'identificatore (changeSessionId) contro la session fixation. I filtri (Filter, FilterChain) elaborano richiesta e risposta prima e dopo le servlet: codifica, autenticazione, log. Il war ha la struttura WEB-INF/classes, WEB-INF/lib, WEB-INF/web.xml, META-INF/context.xml; l'accesso ai dati usa un DataSource con pool di connessioni dichiarato in context.xml e cercato con JNDI, e un DAO con PreparedStatement e try-with-resources.Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni →, Sincronizzazione - sezione critica, race condition e mutexQuando più processi o thread accedono agli stessi dati e il risultato dipende dall'ordine degli accessi si ha una race condition (esempio: counter++ e counter-- sono tre istruzioni macchina ciascuna, e interleaving diversi danno 4, 5 o 6 partendo da 5); il problema della sezione critica chiede un protocollo (entry section, sezione critica, exit section, remainder section) che garantisca mutua esclusione, progresso e attesa limitata; disabilitare gli interrupt non scala sui multiprocessori, l'alternanza stretta con turn viola il progresso, la soluzione di Peterson (flag[2] e turn) soddisfa i tre requisiti ma può fallire su hardware moderno per il riordino delle istruzioni, per cui servono barriere di memoria; l'hardware offre test_and_set e compare_and_swap (atomiche) e variabili atomiche; il mutex lock (acquire/release, in POSIX pthread_mutex_t) è lo strumento di alto livello, con attesa attiva se è uno spinlock oppure con sospensione del thread se è un mutex di sistema; bloccare due volte lo stesso mutex senza sbloccarlo causa deadlock.Sincronizzazione - sezione critica, race condition e mutex →, Organizzazione fisica dei dati e accesso programmatico alle basi di datiUn DBMS è formato da un gestore delle connessioni, un parser, un ottimizzatore che sceglie il piano di esecuzione usando le statistiche del catalogo, un esecutore, i metodi di accesso (heap, ordinato, hash, B-tree, indici), il buffer manager e il gestore delle transazioni (lock e log). Il costo si misura in accessi a blocchi del disco. L'ottimizzatore riscrive l'espressione algebrica (selezioni e proiezioni il prima possibile, prodotto con selezione = join, ordine dei join) e sceglie gli algoritmi: scansione o indice per le selezioni, nested loop, block nested loop, index nested loop, sort-merge o hash join per i join (con 1000 e 100 blocchi, 10 pagine di buffer: 11 mila accessi con block nested loop, 3300 con hash join). La progettazione fisica sceglie organizzazione dei file, indici ed eventuale denormalizzazione in base al carico. Le applicazioni accedono ai dati con SQL incorporato, interfacce a livello di chiamata (ODBC, JDBC, DB-API di Python), ORM o routine memorizzate: connessione, cursore, execute con parametri, fetch, commit o rollback. Mai concatenare l'input dell'utente nell'SQL (SQL injection): query parametrizzate. Un'applicazione web è a tre livelli: browser, server applicativo (HTTP senza stato, sessioni e cookie) e DBMS.Organizzazione fisica dei dati e accesso programmatico alle basi di dati →. Le due servlet sono state compilate contro una copia delle sole firme dell'API Jakarta Servlet; i filtri sono stati provati con oggetti finti; la corsa tra thread è stata eseguita davvero, e i numeri sono quelli misurati.

Testo

Parte 1: lettura del codice. La servlet seguente compila e "funziona" in una prova con un solo utente. Elencare tutti i difetti (almeno sette), dire per ciascuno che cosa può succedere e riscrivere la servlet corretta, usando il DAO PrenotazioneDAO (con il metodo perStudente).

java
package it.unipd.aule.web;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

// ESERCIZIO: quanti difetti ci sono in questa servlet? (compila, ma non va usata)
@WebServlet("/riepilogo")
public class RiepilogoSbagliataServlet extends HttpServlet {
    private String studente;
    private int richieste = 0;
    private Connection conn;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        richieste++;
        studente = req.getParameter("studente");
        String cancella = req.getParameter("cancella");
        resp.getWriter().println("<h1>Prenotazioni di " + studente + " (richiesta n. " + richieste + ")</h1>");
        try {
            conn = DriverManager.getConnection("jdbc:postgresql://db:5432/aule", "aule", "aule");
            Statement st = conn.createStatement();
            if (cancella != null) {
                st.executeUpdate("DELETE FROM prenotazione WHERE id = " + cancella);
            }
            ResultSet rs = st.executeQuery("SELECT giorno, ora_inizio, ora_fine FROM prenotazione WHERE studente = '" + studente + "'");
            while (rs.next()) {
                resp.getWriter().println("<p>" + rs.getString(1) + " " + rs.getString(2) + "-" + rs.getString(3) + "</p>");
            }
        } catch (SQLException e) {
            e.printStackTrace(resp.getWriter());
        }
    }
}

Parte 2: ciclo di vita. (a) Tre richieste GET /riepilogo?studente=Rossi arrivano una dopo l'altra: quante volte vengono eseguiti costruttore, init, service, doGet, destroy? (b) Due richieste arrivano nello stesso istante: quanti oggetti servlet, quanti thread? (c) Con loadOnStartup = 1 che cosa cambia?

Parte 3: sessioni. Dopo il login (POST /login con studente=Rossi) il browser visita /prenotazioni; un secondo browser, senza cookie, visita /prenotazioni. Scrivere gli header scambiati e dire che cosa fa il filtro AutenticazioneFilter. Perché al login si chiama changeSessionId()?

Parte 4: filtri. Descrivere che cosa succede, passo per passo, in CodificaFilter e AutenticazioneFilter nei tre casi: sessione con studente, sessione senza studente, nessuna sessione.

Soluzione

Parte 1: i difetti

# Difetto Che cosa può succedere
1 private String studente: dato della richiesta in un campo condiviso con due utenti insieme la richiesta A scrive il suo nome, la B lo sovrascrive, A mostra le prenotazioni (e il titolo) della persona sbagliata: fuga di dati tra utenti
2 private int richieste incrementato con ++ l'incremento non è atomico (leggi, somma, riscrivi): si perdono aggiornamenti (misura sotto)
3 private Connection conn condivisa; una DriverManager.getConnection per richiesta, mai chiusa; password nel codice le richieste si contendono la stessa connessione; ogni richiesta ne apre una nuova e non la rilascia: il database finisce le connessioni. Si usano il pool (DataSource) e try-with-resources
4 SQL costruito concatenando studente e cancella SQL injection: studente=' OR '1'='1 restituisce tutte le prenotazioni; cancella=1 OR 1=1 cancella l'intera tabella. Query parametrizzate, nel DAO
5 Una GET che modifica i dati (cancella), senza autenticazione né controllo del proprietario un crawler, un'anteprima o un link in un'email cancellano prenotazioni; chiunque cancella quelle di chiunque. Le modifiche si fanno con POST/DELETE e si controlla chi le chiede
6 Il nome dello studente scritto nell'HTML senza escape XSS: studente=<script>...</script> esegue codice nel browser di chi apre il link
7 Nessun Content-Type né codifica; la risposta usa l'impostazione predefinita le lettere accentate possono comparire sbagliate; il browser indovina il tipo
8 e.printStackTrace(resp.getWriter()) e stato 200 anche in caso di errore la pagina mostra all'utente classi, query e indirizzo del database (informazioni utili a un attaccante); il client crede che sia andato tutto bene
9 Nessun controllo di studente == null la pagina dice "Prenotazioni di null" e la query cerca la stringa null

Misura del difetto 2 con 8 thread che incrementano 100000 volte ciascuno un int condiviso e un AtomicInteger (tre esecuzioni):

atteso    : 800000
int       : 282387        269648        285214
atomico   : 800000

L'int perde circa due terzi degli incrementi (i valori cambiano a ogni esecuzione: dipendono dall'alternarsi dei thread); l'AtomicInteger è sempre esatto. Una servlet carica di richieste simultanee ha lo stesso problema.

La servlet corretta:

java
package it.unipd.aule.web;

import it.unipd.aule.dao.PrenotazioneDAO;
import it.unipd.aule.model.Prenotazione;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.concurrent.atomic.AtomicInteger;

/** Riepilogo delle prenotazioni di uno studente (versione corretta di RiepilogoSbagliataServlet). */
@WebServlet("/riepilogo")
public class RiepilogoServlet extends HttpServlet {
    private final AtomicInteger richieste = new AtomicInteger();   // stato condiviso: thread-safe
    private PrenotazioneDAO dao;                                   // condiviso, ma sola lettura dopo init()

    @Override
    public void init() {
        dao = (PrenotazioneDAO) getServletContext().getAttribute("prenotazioneDAO");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studente = req.getParameter("studente");            // variabile LOCALE
        if (studente == null || studente.isBlank()) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "manca il parametro studente");
            return;
        }
        int numero = richieste.incrementAndGet();
        try {
            resp.setContentType("text/html;charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.println("<h1>Prenotazioni di " + Html.esc(studente) + " (richiesta n. " + numero + ")</h1>");
            for (Prenotazione p : dao.perStudente(studente)) {     // query parametrizzata nel DAO
                out.println("<p>" + p.getGiorno() + " " + p.getOraInizio() + "-" + p.getOraFine() + "</p>");
            }
        } catch (SQLException e) {
            log("errore SQL", e);                                  // il dettaglio nel log...
            throw new ServletException("errore di accesso ai dati");   // ...non nella pagina
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // le modifiche (cancellazione) si fanno con POST/DELETE: vedi AnnullaServlet
        resp.setHeader("Allow", "GET");
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
}
  • Il dato della richiesta (studente) è una variabile locale; i soli campi sono il contatore atomico e il DAO (impostato in init, mai più modificato).
  • La query è nel DAO, con PreparedStatement e il pool; non c'è alcun cancella: l'annullamento sta in AnnullaServlet (POST, solo il proprietario).
  • Html.esc per l'XSS; Content-Type con charset=UTF-8 impostato prima di getWriter(); 400 se manca il parametro; il dettaglio dell'errore SQL nel log, un messaggio generico (e 500) all'utente.
  • Un doPost risponde 405 con Allow: GET.

Parte 2: ciclo di vita

(a) Costruttore 1, init 1 (alla prima richiesta, poiché non c'è loadOnStartup), service 3, doGet 3 (service smista in base al metodo HTTP), destroy 0 (si esegue allo spegnimento o al riavvio dell'applicazione, una volta). (b) Un solo oggetto servlet e due thread del container: ciascuno esegue doGet sulla stessa istanza (per questo i campi sono condivisi). (c) Con loadOnStartup = 1 (o @WebServlet(loadOnStartup = 1)) costruttore e init si eseguono all'avvio dell'applicazione e non alla prima richiesta: la prima richiesta non paga il costo dell'inizializzazione e gli errori di init emergono subito.

http
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

studente=Rossi
http
HTTP/1.1 302
Set-Cookie: JSESSIONID=9D2F0C5A6B7E4D1F8A3C2B1E0F9D8C7B; Path=/; HttpOnly
Location: /prenotazioni
http
GET /prenotazioni HTTP/1.1
Cookie: JSESSIONID=9D2F0C5A6B7E4D1F8A3C2B1E0F9D8C7B

Il primo browser rimanda il cookie: il filtro trova la sessione e l'attributo studente e passa la richiesta alla servlet (risposta 200). Il secondo browser non ha cookie: il filtro chiama req.getSession(false), che restituisce null (non crea una sessione), risponde da solo 302 con Location: /login.html e non chiama la servlet.

changeSessionId() al login evita la session fixation: un attaccante potrebbe far usare alla vittima un identificatore di sessione noto a lui (per esempio via URL); se il login mantenesse lo stesso identificatore, l'attaccante, che lo conosce, entrerebbe nella sessione ormai autenticata. Con un identificatore nuovo, quello vecchio non vale più. In più: cookie HttpOnly (non leggibile da JavaScript), Secure in HTTPS, SameSite contro il CSRF.

Parte 4: i filtri

Chiamate osservate nella prova (oggetti finti), per la sola logica del filtro:

Caso AutenticazioneFilter Alla servlet?
sessione con studente getSession(false), getAttribute("studente") (non nullo), poi chain.doFilter sì
sessione senza studente getSession(false), getAttribute (nullo), getContextPath, sendRedirect no
nessuna sessione getSession(false) (nullo), getContextPath, sendRedirect (non si chiama getAttribute) no

CodificaFilter chiama solo req.setCharacterEncoding("UTF-8") e poi chain.doFilter: si applica a tutto (/*), prima che una servlet legga i parametri. L'ordine conta: la codifica va impostata prima di getParameter. Con più filtri sullo stesso URL, l'ordine delle annotazioni non è definito: si fissa con gli elementi filter-mapping del web.xml. Un filtro che blocca la richiesta (non chiama chain.doFilter) deve produrre da solo la risposta, come qui con il redirect.

Versione ripasso

Difetti di una servlet "che funziona con un solo utente" (campo studente, contatore int richieste, connessione condivisa, SQL concatenato, GET che cancella, HTML senza escape...).

# Difetto Conseguenza e rimedio
1 dato della richiesta in un campo (private String studente) richieste che si sovrascrivono, fuga di dati tra utenti →\to variabile locale
2 richieste++ su un int condiviso aggiornamenti persi →\to AtomicInteger
3 Connection in un campo, connessione per richiesta mai chiusa, password nel codice connessioni esaurite →\to DataSource (pool) e try-with-resources
4 SQL concatenato con studente e cancella SQL injection (' OR '1'='1, 1 OR 1=1) →\to query parametrizzate nel DAO
5 GET che modifica, senza controllo del proprietario cancellazioni da crawler o link →\to POST/DELETE e controllo di chi chiede
6 nome scritto nell'HTML senza escape XSS →\to Html.esc
7 niente Content-Type né codifica accenti sbagliati →\to text/html;charset=UTF-8 prima di getWriter()
8 printStackTrace nella pagina, stato 200 per errori informazioni a un attaccante →\to log e 500 generico
9 studente nullo non controllato "Prenotazioni di null" →\to 400

Misura (8 thread ×\times 100000 incrementi, tre esecuzioni): int 282387, 269648, 285214 su 800000 attesi (cambia a ogni esecuzione); AtomicInteger sempre 800000.

java
@WebServlet("/riepilogo")
public class RiepilogoServlet extends HttpServlet {
    private final AtomicInteger richieste = new AtomicInteger();   // stato condiviso thread-safe
    private PrenotazioneDAO dao;                                   // impostato in init(), mai modificato

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studente = req.getParameter("studente");            // LOCALE
        if (studente == null || studente.isBlank()) { resp.sendError(400, "manca il parametro studente"); return; }
        int numero = richieste.incrementAndGet();
        try {
            resp.setContentType("text/html;charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.println("<h1>Prenotazioni di " + Html.esc(studente) + " (richiesta n. " + numero + ")</h1>");
            for (Prenotazione p : dao.perStudente(studente)) { /* una riga per prenotazione */ }
        } catch (SQLException e) {
            log("errore SQL", e);
            throw new ServletException("errore di accesso ai dati");
        }
    }
}

Ciclo di vita. 3 richieste in sequenza: costruttore 1, init 1 (alla prima richiesta), service 3, doGet 3, destroy 0 (allo spegnimento). Due richieste simultanee: un oggetto servlet, due thread. loadOnStartup = 1: costruttore e init all'avvio dell'applicazione.

Sessione. Login POST /login →\to 302 con Set-Cookie: JSESSIONID=...; Path=/; HttpOnly e Location: /prenotazioni; il browser rimanda Cookie: JSESSIONID=.... Il filtro trova la sessione e l'attributo e passa; senza cookie getSession(false) è null (non crea nulla), risponde 302 a /login.html e non chiama la servlet. changeSessionId() al login: contro la session fixation (l'identificatore noto all'attaccante non vale più). HttpOnly, Secure, SameSite.

Filtri (chiamate osservate):

Caso Chiamate di AutenticazioneFilter Alla servlet?
sessione con studente getSession(false), getAttribute, chain.doFilter sì
sessione senza studente getSession(false), getAttribute, getContextPath, sendRedirect no
nessuna sessione getSession(false), getContextPath, sendRedirect no

CodificaFilter: solo req.setCharacterEncoding("UTF-8") e chain.doFilter, su /*, prima che qualcuno legga i parametri. Ordine fra più filtri: filter-mapping nel web.xml. Un filtro che non chiama chain.doFilter deve produrre da solo la risposta.

Errori tipici

  • Dati della richiesta in campi; ++ su campi condivisi.
  • Connessioni non chiuse; SQL concatenato; modifiche con GET.
  • getSession() al posto di getSession(false) nel filtro di controllo.
  • Credere che init si ripeta a ogni richiesta (è una volta sola).

Teoria: Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioniUna servlet è una classe Java che un container (Apache Tomcat: connettore HTTP Coyote, contenitore Catalina, compilatore JSP Jasper) istanzia una sola volta e chiama per ogni richiesta, ciascuna in un thread del pool. Ciclo di vita: caricamento e costruzione, init() una volta, service() per ogni richiesta (HttpServlet lo smista su doGet, doPost, doPut, doDelete), destroy() allo spegnimento. La richiesta (HttpServletRequest) dà metodo, parametri (query string e corpo del form), header, path (contextPath, servletPath, pathInfo) e corpo; la risposta (HttpServletResponse) si costruisce con stato, header e corpo (PrintWriter), con sendRedirect (302 + Location) o sendError. Il mapping si dichiara con @WebServlet o in WEB-INF/web.xml (che prevale). Poiché esiste una sola istanza e molti thread, i campi di istanza sono condivisi: lo stato della richiesta sta in variabili locali, quello condiviso in oggetti thread-safe (AtomicInteger, DataSource). HTTP è senza stato, quindi si usano le sessioni: la servlet crea un HttpSession, tiene i dati sul server e manda al browser solo l'identificatore nel cookie JSESSIONID (Set-Cookie con HttpOnly, Secure, SameSite, Path secondo RFC 6265); al login si cambia l'identificatore (changeSessionId) contro la session fixation. I filtri (Filter, FilterChain) elaborano richiesta e risposta prima e dopo le servlet: codifica, autenticazione, log. Il war ha la struttura WEB-INF/classes, WEB-INF/lib, WEB-INF/web.xml, META-INF/context.xml; l'accesso ai dati usa un DataSource con pool di connessioni dichiarato in context.xml e cercato con JNDI, e un DAO con PreparedStatement e try-with-resources.Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni →.

Teoria collegata