Risolvere il problema di logout in modo corretto ed elegante

Molte applicazioni Web non contengono informazioni eccessivamente riservate e personali come numeri di conti bancari o dati di carte di credito. Ma alcuni contengono dati sensibili che richiedono una sorta di schema di protezione con password. Ad esempio, in una fabbrica in cui i lavoratori devono utilizzare un'applicazione Web per immettere le informazioni sulla scheda attività, accedere ai corsi di formazione e rivedere le tariffe orarie, ecc., L'utilizzo di SSL (Secure Socket Layer) sarebbe eccessivo (le pagine SSL non vengono memorizzate nella cache; il la discussione su SSL va oltre lo scopo di questo articolo). Ma certamente queste applicazioni richiedono un qualche tipo di protezione tramite password. In caso contrario, i lavoratori (in questo caso, gli utenti dell'applicazione) scopriranno informazioni sensibili e riservate su tutti i dipendenti della fabbrica.

Esempi simili alla situazione di cui sopra includono computer dotati di Internet in biblioteche pubbliche, ospedali e Internet café. In questi tipi di ambienti in cui gli utenti condividono alcuni computer comuni, la protezione dei dati personali degli utenti è fondamentale. Allo stesso tempo, le applicazioni ben progettate e ben implementate non danno per scontato nulla sugli utenti e richiedono la minima quantità di formazione.

Vediamo come si comporterebbe un'applicazione Web perfetta in un mondo perfetto: un utente punta il proprio browser a un URL. L'applicazione Web visualizza una pagina di accesso che chiede all'utente di immettere una credenziale valida. Digita l'ID utente e la password. Supponendo che le credenziali fornite siano corrette, dopo il processo di autenticazione, l'applicazione Web consente all'utente di accedere liberamente alle sue aree autorizzate. Quando è il momento di uscire, l'utente preme il pulsante Logout della pagina. L'applicazione Web visualizza una pagina che chiede all'utente di confermare che desidera effettivamente disconnettersi. Dopo aver premuto il pulsante OK, la sessione termina e l'applicazione Web presenta un'altra pagina di accesso. L'utente può ora allontanarsi dal computer senza preoccuparsi che altri utenti accedano ai suoi dati personali. Un altro utente si siede allo stesso computer. Preme il pulsante Indietro;l'applicazione Web non deve mostrare nessuna delle pagine dell'ultima sessione dell'utente. Infatti, l'applicazione Web deve sempre mantenere intatta la pagina di login fino a quando il secondo utente non fornisce una credenziale valida, solo allora può visitare la sua area autorizzata.

Tramite programmi di esempio, questo articolo mostra come ottenere tale comportamento in un'applicazione Web.

Campioni JSP

Per illustrare in modo efficiente la soluzione, questo articolo inizia mostrando i problemi riscontrati nell'applicazione Web, logoutSampleJSP1 . Questa applicazione di esempio rappresenta un'ampia gamma di applicazioni Web che non gestiscono correttamente il processo di disconnessione. logoutSampleJSP1 consiste delle seguenti pagine JSP (JavaServer Pages): login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, e logoutAction.jsp. Le pagine JSP home.jsp, secure1.jsp, secure2.jsp, e logout.jspsono protetti contro gli utenti non autenticati, cioè, che contengono informazioni sicure e non dovrebbero mai apparire sul browser sia prima che l'utente accede a, o dopo che l'utente si disconnette. La pagina login.jspcontiene un modulo in cui gli utenti digitano il proprio nome utente e password. La paginalogout.jspcontiene un modulo che chiede agli utenti di confermare che desiderano effettivamente disconnettersi. Le pagine JSP loginAction.jspe logoutAction.jspagire come controllori e contengono codice che esegue le azioni di login e logout, rispettivamente.

Una seconda applicazione Web di esempio, logoutSampleJSP2 , mostra come risolvere il problema di logoutSampleJSP1. Tuttavia, logoutSampleJSP2 rimane problematico. Il problema di logout può ancora manifestarsi in una circostanza speciale.

Una terza applicazione Web di esempio, logoutSampleJSP3 , migliora dopo logoutSampleJSP2 e rappresenta una soluzione accettabile al problema di logout.

Un ultimo esempio di applicazione Web logoutSampleStruts mostra come Jakarta Struts può risolvere elegantemente il problema di logout.

Nota: gli esempi che accompagnano questo articolo sono stati scritti e testati per i browser Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox e Avant più recenti.

Azione di accesso

L'eccellente articolo di Brian Pontarelli "J2EE Security: Container Versus Custom" discute diversi approcci di autenticazione J2EE. A quanto pare, gli approcci di autenticazione HTTP di base e basati su form non forniscono un meccanismo per la gestione del logout. La soluzione quindi consiste nell'utilizzare un'implementazione di sicurezza personalizzata, poiché fornisce la massima flessibilità.

Una pratica comune nell'approccio dell'autenticazione personalizzata consiste nel recuperare le credenziali utente dall'invio di un modulo e confrontarle con gli ambiti di sicurezza back-end come LDAP (protocollo di accesso alla directory leggero) o RDBMS (sistema di gestione del database relazionale). Se la credenziale fornita è valida, l'azione di accesso salva alcuni oggetti HttpSessionnell'oggetto. La presenza di questo oggetto in HttpSessionindica che l'utente ha effettuato l'accesso all'applicazione Web. Per maggiore chiarezza, tutte le applicazioni di esempio di accompagnamento salvano solo la stringa del nome utente nel HttpSessionper denotare che l'utente ha effettuato l'accesso. Il listato 1 mostra uno snippet di codice contenuto nella pagina loginAction.jspper illustrare l'azione di accesso:

Listato 1

// ... // inizializza l'oggetto RequestDispatcher; imposta inoltro alla home page per impostazione predefinita RequestDispatcher rd = request.getRequestDispatcher ("home.jsp"); // Preparare la connessione e l'istruzione rs = stmt.executeQuery ("select password from USER where userName = '" + userName + "'"); if (rs.next ()) {// La query restituisce solo 1 record nel set di risultati; solo 1 password per userName che è anche la chiave primaria if (rs.getString ("password"). equals (password)) {// If valid password session.setAttribute ("User", userName); // Salva la stringa del nome utente nell'oggetto sessione} else {// La password non corrisponde, ad esempio, richiesta di password utente non valida.setAttribute ("Error", "Invalid password."); rd = request.getRequestDispatcher ("login.jsp"); }} // Nessun record nel set di risultati, ovveronome utente non valido altro {request.setAttribute ("Errore", "Nome utente non valido."); rd = request.getRequestDispatcher ("login.jsp"); }} // Come controller, loginAction.jsp alla fine inoltra a "login.jsp" o "home.jsp" rd.forward (richiesta, risposta); // ...

In questa e nel resto delle applicazioni Web di esempio associate, si presume che l'ambito della sicurezza sia un RDBMS. Tuttavia, il concetto di questo articolo è trasparente e applicabile a qualsiasi ambito di sicurezza.

Azione di logout

L'azione di logout implica semplicemente la rimozione della stringa del nome utente e la chiamata del invalidate()metodo HttpSessionsull'oggetto dell'utente . Il listato 2 mostra uno snippet di codice contenuto nella pagina logoutAction.jspper illustrare l'azione di logout:

Listato 2

// ... session.removeAttribute ("Utente"); session.invalidate (); // ...

Impedisci l'accesso non autenticato alle pagine JSP protette

Per ricapitolare, dopo una convalida riuscita delle credenziali recuperate dall'invio del modulo, l'azione di accesso inserisce semplicemente una stringa di nome utente HttpSessionnell'oggetto. L'azione di logout fa l'opposto. Rimuove la stringa del nome utente da HttpSessione chiama il invalidate()metodo HttpSessionsull'oggetto. Affinché entrambe le azioni di login e logout siano significative, tutte le pagine JSP protette devono prima controllare la stringa del nome utente contenuta in HttpSessionper determinare se l'utente è attualmente connesso. Se HttpSessioncontiene la stringa del nome utente, un'indicazione che l'utente è connesso. l'applicazione Web invierà ai browser il contenuto dinamico nel resto della pagina JSP. In caso contrario, la pagina JSP trasmetterà la schiena flusso di controllo alla pagina di login, login.jsp. Le pagine JSP home.jsp, secure1.jsp,secure2.jspe logout.jspcontengono tutti lo snippet di codice mostrato nel listato 3:

Listato 3

// ... String userName = (String) session.getAttribute ("User"); if (null == userName) {request.setAttribute ("Error", "La sessione è terminata. Effettua il login."); RequestDispatcher rd = request.getRequestDispatcher ("login.jsp"); rd.forward (richiesta, risposta); } // ... // Consenti al resto del contenuto dinamico di questo JSP di essere servito al browser // ...

Questo frammento di codice recupera la stringa del nome utente da HttpSession. Se la stringa del nome utente recuperata è nulla , l'applicazione Web si interrompe inoltrando il flusso di controllo alla pagina di accesso con il messaggio di errore "La sessione è terminata. Effettua il login.". In caso contrario, l'applicazione Web consente un flusso normale attraverso il resto della pagina JSP protetta, consentendo così di servire il contenuto dinamico di quella pagina JSP.

Esecuzione di logoutSampleJSP1

L'esecuzione di logoutSampleJSP1 produce il seguente comportamento:

  • Le si comporta applicazione correttamente, impedendo il contenuto dinamico delle pagine JSP protette home.jsp, secure1.jsp, secure2.jsp, e logout.jspda essere servito se l'utente non ha effettuato l'accesso. In altre parole, assumendo che l'utente non ha effettuato l'accesso, ma i punti del browser per gli URL quelle pagine JSP , l'applicazione Web inoltra il flusso di controllo alla pagina di accesso con il messaggio di errore "Sessione terminata. Effettua il login.".
  • Allo stesso modo, gli si comporta correttamente applicazione impedendo il contenuto dinamico delle pagine JSP protette home.jsp, secure1.jsp, secure2.jsp, e logout.jspda essere servito dopo che l'utente ha già effettuato il logout. In altre parole, dopo che l'utente si è già disconnesso, se punta il browser agli URL di quelle pagine JSP, l'applicazione Web inoltrerà il flusso di controllo alla pagina di accesso con il messaggio di errore "Sessione terminata. Effettua il login. ".
  • L'applicazione non si comporta correttamente se, dopo che l'utente si è già disconnesso, fa clic sul pulsante Indietro per tornare alle pagine precedenti. Le pagine JSP protette riappaiono nel browser anche al termine della sessione (con la disconnessione dell'utente). Tuttavia, la selezione continua di qualsiasi collegamento in queste pagine porta l'utente alla pagina di accesso con il messaggio di errore "Sessione terminata. Effettua il login.".

Impedisci ai browser di memorizzare nella cache

La radice del problema è il pulsante Indietro che esiste sulla maggior parte dei browser moderni. Quando si fa clic sul pulsante Indietro, il browser per impostazione predefinita non richiede una pagina dal server Web. Invece, il browser ricarica semplicemente la pagina dalla sua cache. Questo problema non è limitato alle applicazioni Web basate su Java (JSP / servlets / Struts); è anche comune a tutte le tecnologie e influisce sulle applicazioni Web basate su PHP (Hypertext Preprocessor), ASP, (Active Server Pages) e .Net.

Dopo che l'utente fa clic sul pulsante Indietro, non viene effettuato alcun viaggio di ritorno ai server Web (in generale) o ai server delle applicazioni (nel caso di Java). L'interazione avviene tra l'utente, il browser e la cache. Quindi, anche con la presenza di Codice 3 di nelle pagine JSP protette come ad esempio home.jsp, secure1.jsp, secure2.jsp, e logout.jsp, questo codice non ha la possibilità di eseguire quando il pulsante Indietro viene cliccato.

A seconda di chi chiedi, le cache che si trovano tra i server delle applicazioni e i browser possono essere una cosa buona o cattiva. Queste cache offrono in effetti alcuni vantaggi, ma questo è principalmente per le pagine HTML statiche o le pagine che richiedono molta grafica o immagini. Le applicazioni web, d'altra parte, sono più orientate ai dati. Poiché è probabile che i dati in un'applicazione Web cambino frequentemente, è più importante visualizzare dati aggiornati che risparmiare un po 'di tempo di risposta andando nella cache e visualizzando informazioni obsolete o non aggiornate.

Fortunatamente, le intestazioni HTTP "Expires" e "Cache-Control" offrono ai server delle applicazioni un meccanismo per controllare le cache dei browser e dei proxy. L'intestazione HTTP Expires impone alle cache dei proxy quando scadrà la "freschezza" della pagina. L'intestazione HTTP Cache-Control, nuova nella specifica HTTP 1.1, contiene attributi che indicano ai browser di impedire la memorizzazione nella cache su qualsiasi pagina desiderata nell'applicazione Web. Quando il pulsante Indietro rileva una pagina di questo tipo, il browser invia la richiesta HTTP al server delle applicazioni per una nuova copia di quella pagina. Di seguito sono riportate le descrizioni delle direttive delle intestazioni di Cache-Control necessarie:

  • no-cache: impone alle cache di ottenere una nuova copia della pagina dal server di origine
  • no-store: indirizza le cache a non memorizzare la pagina in nessuna circostanza

Per compatibilità con le versioni precedenti di HTTP 1.0, la Pragma:no-cachedirettiva, che è equivalente a Cache-Control:no-cacheHTTP 1.1, può anche essere inclusa nella risposta dell'intestazione.

Sfruttando le direttive cache delle intestazioni HTTP, la seconda applicazione Web di esempio, logoutSampleJSP2, che accompagna questo articolo rimedia a logoutSampleJSP1. logoutSampleJSP2 differisce da logoutSampleJSP1 in quel frammento di codice di Inserzione 4 è posto nella parte superiore di tutte le pagine JSP protette, come home.jsp, secure1.jsp, secure2.jspe logout.jsp:

Listato 4

// ... response.setHeader ("Cache-Control", "no-cache"); // Forza le cache a ottenere una nuova copia della pagina dal server di origine response.setHeader ("Cache-Control", "no-store"); // Indica alle cache di non memorizzare la pagina in nessuna circostanza response.setDateHeader ("Expires", 0); // Fa sì che la cache del proxy veda la pagina come "obsoleta" response.setHeader ("Pragma", "no-cache"); // Compatibilità con le versioni precedenti di HTTP 1.0 String userName = (String) session.getAttribute ("User"); if (null == userName) {request.setAttribute ("Error", "La sessione è terminata. Effettua il login."); RequestDispatcher rd = request.getRequestDispatcher ("login.jsp"); rd.forward (richiesta, risposta); } // ...