L'utilizzo dei filtri, nelle applicazioni Web, è stato promosso, a "standard de jure", con l'avvento delle specifiche 2.3 delle Servlet, sebbene alcuni produttori di Application Server avessero già in precedenza fornito delle funzionalità simili, anche se differenti l'una dall'altra.
Cosa sono i Filtri?
Supponiamo di aver scritto una Servlet che gestisca l'autenticazione degli utenti relativamente ad una certa applicazione Web. Probabilmente, avremo costruito la nostra servlet in modo che verifichi l'esistenza delle credenziali di accesso fornite dagli utenti, interrogando un DataBase e, in caso positivo, istanziando una nuova sessione per ogni utente riconosciuto.
Supponiamo, adesso, che la nostra applicazione Web necessiti di tenere traccia di tutti i tentativi di accesso, da parte degli utenti (sia quelli riconosciuti dal sistema che quelli errati o potenzialmente illeciti), a causa di un presunto tentativo di login con credenziali non autenticate.
La prima cosa che ci viene in mente è quella di modificare la nostra Servlet in modo che scriva da qualche parte tali informazioni. La modifica (semplice o meno che sia) implicherà, comunque, una nuova operazione di deployment sull'Application Server.
In futuro si potrebbe presentare la necessità di tenere traccia di ulteriori informazioni, come, ad esempio, il numero di accessi effettuati verso una particolare tabella del DB. Anche in questo caso, saremo costretti a rieditare la nostra servlet, apportare la modifica necessaria e rieseguire il deployment su server. Il rischio, a lungo andare, è quello di popolare il codice con della logica che vada ben oltre lo scopo fondamentale della nostra servlet, ovvero quello di accettare delle richieste e fornire delle risposte ai client.
Lo scenario appena descritto è uno dei classici esempi in cui l'utilizzo di un filtro casca a fagiolo. I filtri rappresentano un modo per fornire una funzionalità aggiuntiva a una applicazione Web.
La potenza dei filtri risiede nel fatto che essi consentono di cambiare il comportamento delle applicazioni Web agendo sul deployment descriptor e sollevando il programmatore dalla necessità di modificare ogni volta il codice (e rieffettuare il deployment dell'applicazione).
Quando usare i Filtri
Ci sono svariate circostanze in cui può rivelarsi utile l'utilizzo dei filtri.
Volendo fare una stima possiamo dire che i filtri più diffusi, probabilmente, sono:
- Filtri di Autenticazione (Authentication filters)
- Filtri di Logging e Auditing
- Filtri per la compressione dei dati (Data compression Filters)
- Filtri di criptazione (Encryption Filters)
- Filtri di conversione delle immagini (Image conversion Filters)
Altri tipi di filtri, comunemente utilizzati, rientrano nelle seguenti categorie:
- Tokenizing Filters
- Filtri che innescano eventi di accesso alle risorse
- XSL e XSLT Filters
- Mime-type chain Filters
Se si ritiene che la nostra applicazione Web debba fornire una specifica funzionalità che esuli da quelle elencate, nulla ci vieterà di costruirci un filtro ad hoc in cui incapsularne le specifiche.
Un altro vantaggio non trascurabile derivante dall'utilizzo dei filtri è che essi sono riutilizzabili facilmente anche da altre servlet.
Come si implementa un Filtro
Per utilizzare un filtro all'interno di un'applicazione Web sono necessarie due cose:
- Scrivere una classe che implementi l'interfaccia
javax.servlet.Filter
- Modificare il Deployment Descriptor per istruire il container relativamente alle modalità di utilizzo del filtro che abbiamo definito
Per prima cosa andiamo a scoprire quali sono i metodi definiti nell'interfaccia javax.servlet.Filter
:
void init(FilterConfig filterConfig)
: Viene invocato, dal web container, subito dopo la creazione dell'istanza di un filtro e appena prima della messa in servizio del filtro stesso.void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
: Viene invocato, dal web container, e contiene l'implementazione vera e propria del filtro.void destroy()
: Viene invocato, dal web container, per indicare ad un filtro il termine del suo ciclo di vita.
Si nota una certa somiglianza tra l'interfaccia Filter e l'interfaccia Servlet. Tale somiglianza non è casuale ma rispecchia un comportamento comune per quanto riguarda il ciclo di vita di questi due componenti.
Quando viene creato un filtro, il container si occuperà di invocare il metodo init()
, all'interno del quale sarà possibile accedere ai parametri di inizializzazione forniti attraverso l'interfaccia javax.servlet.FilterConfig
. I metodi definiti da quest'ultima interfaccia sono i seguenti:
String getFilterName()
: Restituisce il nome del filtro in relazione a quello definito nel deployment descriptorServletContext getServletContext()
: Restituisce un riferimento al ServletContext del chiamanteString getInitParameter(String name)
: Restituisce una stringa contenente il valore del parametro di inizializzazione il cui nome è quello contenuto nel parametro name.Enumeration getInitParameterNames()
: Restituisce i nomi dei parametri di inizializzazione del filtro attraverso un Enumeration di oggetti di tipo stringa.
Torniamo al ciclo di vita dei filtri. Per soddisfare le richieste pervenute, il container invoca il metodo doFilter()
e, al termine del ciclo di vita del filtro stesso, richiama il metodo destroy()
. Come è possibile osservare, il metodo doFilter()
definisce al suo interno un parametro chain
, di tipo FilterChain, che fornisce un riferimento alla catena di filtri a cui la richiesta iniziale dovrà essere sottoposta. Infatti, è possibile filtrare le richieste pervenute dai client con una sorta di catena di filtri (Figura 1) che prevede l'invocazione a cascata di tutti i filtri coinvolti nella catena stessa.
Naturalmente, nei casi più semplici, la catena potrà essere composta da un unico anello che richiamerà direttamente la servlet indicata nella richiesta del client.
L'unico metodo definito nell'interfaccia javax.servlet.FilterChain
è il seguente:
void doFilter(ServletRequest request, ServletResponse response)
: Richiama il successivo Filtro da invocare, nella catena di filtri definita dal parametro chain. Nel caso in cui il filtro chiamante sia l'ultimo, o l'unico, della catena allora verrà richiamata la risorsa posta al termine della catena stessa.
Dal punto di vista del codice, all'interno del metodo doFilter()
di un filtro, quando si esegue l'istruzione chain.doFilter()
, verrà richiamato il successivo filtro definito all'interno della catena. Il codice che precede tale invocazione verrà eseguito sempre prima di passare il controllo al successivo componente. Le istruzioni successive alla chain.doFilter()
si occupano (quando presenti) di processare la risposta ottenuta. La figura seguente illustra uno scenario possibile:
Il secondo passo necessario all'implementazione di un filtro è costituito dalle modifiche da apportare al deployment descriptor. In esso sarà necessario riportare le informazioni utili al container per capire se siano presenti dei filtri e, in caso affermativo, quali e come essi siano associati ai componenti web dell'applicazione.
I tag utilizzati per tale scopo sono due: <filter>
e <filter-mapping>
.
Listato 1. Forma di un elemento filter
<filter>
<icon>contiene il path verso un file di tipo icona</icon>
<filter-name>Il nome del filtro</filter-name>
<display-name>Il nome del filtro visualizzato dai tool di gestione</display-name>
<description>Una descrizione del filtro</description>
<filter-class>Il nome completo della classe del filtro</filter-class>
<init-param>
<param-name>il nome di un parametro di inizializzazione del filtro</param_name>
<param-value>il valore del parametro di inizializzazione</param-value>
</init-param>
</filter>
In realtà, soltanto due dei sotto elementi di <filter>
descritti sono obbligatori nel deployment descriptor: <filter-name>
e <filter-class>
. Se si fa uso del tag <init-param>
, però, diventano obbligatori anche <param-name>
e <param-value>
. Le informazioni contenute all'interno del tag <init-param>
saranno accessibili attraverso l'oggetto di tipo FilterConfig ricevuto in input dal metodo init()
del filtro.
Listato 2. Forma di un elemento filter-mapping
<filter-mapping>
<filter-name>Lo stesso nome utilizzato all'interno del tag filter</filter name>
<url-pattern>La URL del componente a cui applicare il filtro</url-pattern>
</filter-mapping>
oppure
Listato 3. Forma di un elemento filter-mapping per una servlet
<filter-mapping>
<filter-name> Lo stesso nome utilizzato all'interno del tag filter </filter name>
<servlet-name>la servlet a cui verrà applicator il filtro</servlet-name>
</filter-mapping>
È fondamentale che i tag presenti nel deployment descriptor seguano rigorosamente l'ordine specificato nel Document Type Definition (DTD). In particolare, tutti i tag di tipo <filter>
devono essere inseriti prima dei tag di tipo <filter-mapping>
. Questi ultimi, a loro volta, devono precedere i tag di tipo <servlet>
. Nel caso di filtri a catena da applicare ad una richiesta, sarà necessario specificare ogni filtro in un elemento di tipo <filter-mapping>
, tenendo presente che l'ordine in cui i filtri verranno applicati sarà lo stesso che verrà introdotto nel deployment descriptor. Ad esempio, se il deployment descriptor fosse costituito nel seguente modo:
Listato 4. Esempio di deployment descriptor
<filter-mapping>
<filter-name>FiltroX</filter name>
<servlet-name>MyServlet</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>FiltroY</filter name>
<servlet-name>MyServlet</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>FiltroZ</filter name>
<servlet-name>MyServlet</servlet-name>
</filter-mapping>
ogni richiesta verso la Servlet denominata Login sarebbe prima intercettata dal filtro FiltroX; Quest'ultimo richiamando il metodo chain.doFilter()
attiverà, in sequenza, il filtro FiltroY
che, a seguire, attiverà il filtro FiltroZ
. Soltanto all'invocazione di chain.doFilter()
da parte del filtro FiltroZ
verrà, finalmente, richiamata la Servlet MyServlet
.
Nella parte precedente dell'articolo abbiamo visto la teoria di funzionamento dei filtri. In questa parte, invece, vedremo un esempio pratico di funzionamento.
Un esempio pratico
Per comprendere i concetti espressi finora creiamo una semplice applicazione Web basata su una servlet (FilteredLogin) la cui richiesta, proveniente da una pagina html di input (login.html), viene preventivamente filtrata da due filtri denominati rispettivamente FiltroA
e FiltroB
. Scopo di tale applicazione è semplicemente quello di mostrare l'ordine di esecuzione dei vari componenti che costituiscono un'applicazione Web soggetta a filtri, attraverso dei semplici messaggi di testo.
Il passo successivo (lasciato al lettore per esercizio) è quello di costruirsi dei filtri personalizzati che facciano qualcosa di più concreto.
Listato 5. login.html: pagina html iniziale
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Una Applicazione Thread Unsafe</title>
</head>
<body>
<h1>Login</h1>
<p>Inserire nei campi sottostanti la propria UserID e Password:</p>
<form action="FilteredLogin" method="POST">
<p><input type="text" name="userID" length="40" /></p>
<p><input type="password" name="password" length="40" /></p>
<p><input type="submit" value="Invia" /></p>
</form>
</body>
</html>
Listato 6. FilteredLogin.java: Servlet soggetta ai filtri (Vedi codice completo)
...
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
System.out.println("La Servlet sta processando il metodo doPost");
String userID = request.getParameter("userID");
String password = request.getParameter("password");
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><body>");
writer.println("Benvenuto " + userID + "!");
writer.println("</body></html>");
writer.close();
}
...
Il primo filtro ad essere richiamato è il filtro denominato FiltroA
. All'interno del metodo doFilter()
è ben visibile la divisione del codice nei tre punti principali:
- Precedente alla invocazione al successivo filtro (FiltroB)
- Chiamata al filtro successivo (FiltroB)
- Successiva alla invocazione al filtro FiltroB
Listato 7. FiltroA.java: primo filtro (Vedi codice completo)
...
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
System.out.println("Il filtro FiltroA ha ricevuto la richiesta dal client");
System.out.println("Il filtro FiltroA smista la richiesta al filtro FiltroB");
try
{
chain.doFilter(request, response);
}
catch(Exception ex)
{
ex.printStackTrace();
}
System.out.println("La servlet ha terminato la sua azione");
System.out.println("Il filtro FiltroA è pronto per processare la risposta " +
"ricevuta dalla servlet");
...
System.out.println("Il filtro FiltroA è pronto per processare la " +
"risposta ricevuta dal filtro FiltroB");
...
Analoghe considerazioni valgono per il filtro FiltroB
. In questo caso, però, essendo l'ultimo della catena, la successiva invocazione sarà rivolta direttamente alla Servlet.
Listato 8. FiltroB.java: ultimo filtro della catena (Vedi codice completo)
...
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
System.out.println("Inizio del metodo doFilter del filtro FiltroB");
System.out.println("Il client remoto è " + request.getRemoteHost());
System.out.println("La userID inserita è " +
request.getParameter("userID"));
System.out.println("La password inserita è " +
request.getParameter("password"));
...
System.out.println("La servlet ha terminato la sua azione");
System.out.println("Il filtro FiltroB ripassa il controllo al " +
"filtro FiltroA");
...
Non ci rimane che scrivere il deployment descriptor secondo le indicazioni osservate in precedenza:
Listato 9. Web.xml: deployment descriptor valido per Tomcat 5.5 (Vedi codice completo)
...
<filter>
<filter-name>FiltroA</filter-name>
<filter-class>web.FiltroA</filter-class>
</filter>
...
<filter-mapping>
<filter-name>FiltroB</filter-name>
<url-pattern>/FilteredLogin</url-pattern>
</filter-mapping>
<servlet>
<description></description>
<display-name>FilteredLogin</display-name>
<servlet-name>FilteredLogin</servlet-name>
<servlet-class>web.FilteredLogin</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FilteredLogin</servlet-name>
<url-pattern>/FilteredLogin</url-pattern>
</servlet-mapping>
...
Effettuiamo il deployment dell'applicazione e mandiamola in esecuzione. Inseriamo i dati richiesti dalla pagina html iniziale, clicchiamo sul pulsante "Invia" e andiamo a vedere la console di Java.