Novembre 24, 2024

Controllo dell’autenticazione con Spring MVC e Handler Interceptor

Nel contesto di un’applicazione web Spring MVC, l’oggetto HandlerInterceptor sostituisce il concetto del tradizionale Servlet Filter J2EE. Il suo scopo è quello di rimanere in ascolto e intercettare tutte le URL mappate sull’applicazione e processate da un Handler. In termini di programmazione event-driven, gli Handler sono dei gestori di eventi che vengono richiamati per gestire e rispondere ad un determinato evento come può essere per esempio un processo d’invocazione Request-Response di un servizio web. L’oggetto HandlerInterceptor si inserisce nel ciclo di vita di un Handler ed è in grado di effettuare delle operazioni indipendenti e in maniera completamente disaccoppiata da esso. HandlerInterceptor definisce tre metodi per gestire operazioni sulla chain-execution di un Handler:

– preHandle: richiamato prima che l’Handler corrente venga eseguito
– postHandle: richiamato dopo l’esecuzione dell’Handler corrente ma prima che la vista venga renderizzata
– afterCompletion: richiamato al completamento di tutto il flusso Request/Response

In questo articolo mostro un esempio di come effettuare un controllo sull’autenticazione dell’utente utilizzando HandlerInterceptor in un’applicazione Spring MVC. Ovviamente esistono altre soluzioni più sicure e affidabile che implementano il processo di autenticazione e access-control (vedi per esempio Spring Security). Tuttavia questo esempio può essere molto utile per capire il concetto di Interceptor e di chain-execution in Spring MVC. Per avere un’idea di quello che succede a livello temporale, ho abbozzato alcuni sequence diagram isolando i principali casi d’uso relativamente al meccanismo di autenticazione.

4 - Request for any other URI

Immagine 4 di 4


SOURCE CODE (/giuseu/spring-mvc)

GIT
git clone https://gitlab.com/giuseppeurso-eu/spring-mvc

ROADMAP
STEP 1. Configurazione Spring MVC
STEP 2. Configurazione Login
STEP 3. Configurazione Interceptor

STEP 1. Configurazione Spring MVC

Creo lo skeleton dell’applicazione con Maven e aggiungo nel pom.xml le dipendenze per Spring MVC. Importo il progetto in Eclipse e imposto su di esso “Project Facets” > “Dynamic Web Module”. Questo mi permette di pubblicare l’applicazione di esempio dentro il Tomcat integrato in Eclipse e di effettuare facilmente attività di debugging. Preparo la struttura del progetto e aggiungo alcune risorse per la componente di visualizzazione delle pagine come css, immagini e librerie javascript.

$ mvn archetype:generate 
-DgroupId=eu.giuseppeurso.sampleintercep 
-DartifactId=sample-intercep
-DarchetypeArtifactId=maven-archetype-quickstart 
-DinteractiveMode=false

 

giuseppe-urso-spring-mvc-and-interceptor-01

Definisco e configuro la Servlet Dispatcher di Spring MVC. Grazie al parametro contextConfigLocation posso definire per i file di configurazione di spring, un path personalizzato che punta alla cartella WEB-INF/spring. Ecco un’estratto del file web.xml e spring-dispatcher.xml

<!-- web.xml -->
<servlet>
    <servlet-name>spring-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/spring-dispatcher.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring-dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
<!-- spring-dispatcher.xml -->

<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<context:component-scan base-package="eu.giuseppeurso.sampleinterc" />

	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/* directory -->
	<resources mapping="/css/**" location="/css/" />
	<resources mapping="/js/**" location="/js/" />
	<resources mapping="/images/**" location="/images/" />
	<resources mapping="/jquery-ui-1.9.2/**" location="/jquery-ui-1.9.2/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 
		<beans:property name="prefix" value="/WEB-INF/views/" /> 
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

Creo l’instanza di Spring MVC Controller e configuro una RequestMapping che porta a una pagina di benvenuto welcome.jsp.

//SampleController.java
@Controller
public class SampleController {

	private static final Logger logger = LoggerFactory.getLogger(SampleController.class);

	@RequestMapping(value = "/welcome", method = RequestMethod.GET)
	  public String welcome() {
	    return "welcome";
	  }	
}
<!-- welcome.jsp -->
<div>
	<h1>WELCOME!
     Spring MVC Interceptor Sample</h1>
</div>

 

giuseppe-urso-spring-mvc-and-interceptor-03

 

STEP 2. Configurazione Login Form

Modifico il Controller per far puntare il context-root dell’applicazione a un form di login (http://localhost:8080/spring-interc/). Il Model di Spring mi servirà successivamente per avere accesso agli attributi del form di login. Creo inoltre il bean di login con gli attributi username e password e la pagina jsp per il form. Il modelAttribute del tag form:form deve avere lo stesso nome utilizzato nel Controller (“loginAttribute“).

// SampleController.java
@RequestMapping(value = "/", method = RequestMethod.GET)
	public String showLogin(Model model, LoginForm loginForm) {
		logger.info("Login page");
		model.addAttribute("loginAttribute", loginForm);
		return "login";
	}
// LoginForm.java
public class LoginForm {

	private String username;
	private String password;

	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}	
}
<!-- login.jsp -->
<form:form action="login" method="post"  modelAttribute="loginAttribute">
	<table border="0" cellpadding="0" cellspacing="0">
		<tr>
			<th>Username</th>
			<td><form:input type="text"  class="login-inp" path="username" /></td>
		</tr>
		<tr>
			<th>Password</th>
			<td><form:input type="password" value="************"  onfocus="this.value=''" class="login-inp" path="password"/></td>
		</tr>
		<tr>
			<th></th>
			<td valign="top"><input type="checkbox" class="checkbox-size" id="login-check" /><label for="login-check">Remember me</label></td>
		</tr>
		<tr>
			<th></th>
			<td><input type="submit" class="submit-login"  /></td>
		</tr>
	</table>
</form:form>

 

STEP 3. Configurazione Interceptor

Definisco nel file spring-dispatcher.xml la mia implementazione di HandlerInterceptor che sarà in ascolto su tutte le url applicative. Per effettuare il controllo sull’autenticazione dell’utente faccio l’overriding del metodo preHandle. Utilizzo l’attributo LOGGEDIN_USER come flag per verificare se la sessione contiene o meno un utente correttamente autenticato. Inoltre faccio un controllo sull’url corrente per evitare dei redirect loop nel caso di accesso alla pagina di default oppure di submit login corretto o errato (vedi sequence diagram precedenti).

<!-- spring-dispatcher.xml-->
<interceptors>
	<interceptor>
		<mapping path="/*"/>
		<beans:bean class="eu.giuseppeurso.sampleinterc.interceptor.AuthenticationInterceptor" />
	</interceptor>
</interceptors>
// AuthenticationInterceptor.java
@Override
	public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {

		log.info("Interceptor: Pre-handle");

		// Avoid a redirect loop for some urls
		if( !request.getRequestURI().equals("/sample-interc/") &&
		    !request.getRequestURI().equals("/sample-interc/login.do") &&
		    !request.getRequestURI().equals("/sample-interc/login.failed"))
		  {
			  LoginForm userData = (LoginForm) request.getSession().getAttribute("LOGGEDIN_USER");
		   if(userData == null)
		   {
		    response.sendRedirect("/sample-interc/");
		    return false;
		   }   
		  }
		  return true;

Infine aggingo tre RequestMapping nel mio Controller per mappare i casi d’uso mostrati nei sequence diagram. Il metodo di tipo POST controlla username e password inseriti nel form di login. Per motivi di praticità, non configuro un Persistence Layer basato su DB. Puttosto simulo lo strato di dati con un semplice file di testo in cui inserisco dei valori per username e password. Per accedere al file utilizzo un semplice ResourceBundle di java.util. All’avvio del Tomcat, vengono inizializzati l‘HandlerInterceptor e tutte le RequestMapping che ho definito nel mio Controller. Faccio puntare il browser al root-context applicativo http://localhost:11000/sample-interc/. La request viene intercettata da AuthenticationInterceptor che mostra la pagina di login. L’interceptor resterà sempre in ascolto su tutte le url gestite dall’Handler, per verificare la presenza o meno in Sessione di un utente correttamente autenticato.

//SampleController.java
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
	public String login(Model model, LoginForm loginform, Locale locale, HttpServletRequest request) throws Exception {

		String username = loginform.getUsername();
		String password = loginform.getPassword();

		// A simple authentication manager
		if(username != null && password != null){

			if( username.equals(settings.getString("username")) &&	password.equals(settings.getObject("password")) ){
				//return "welcome"; 
				request.getSession().setAttribute("LOGGEDIN_USER", loginform);
				return "redirect:/welcome";
			}else{
				return "redirect:/login.failed";
			}
		}else{
			return "redirect:/login.failed";
		}	
	}

 

giuseppe-urso-spring-mvc-and-interceptor-04

 giuseppe-urso-spring-mvc-and-interceptor-05

 

Related posts

2 Comments

  1. Davide

    Ciao Giuseppe, grazie dell’articolo.
    Tu dici che esistono altre soluzioni più sicure e affidabili che implementano il processo di autenticazione e access-control (vedi per esempio Spring Security). Ma perché è più sicuro rispetto ad un Interceptor?
    Io sto implementanto dei RESTful webservices e non MVC, ma comunque non vedo alcuna limitazione se una risorsa /xxx/** è intercettata da un interceptor senza Spring security. Puoi spiegare questo punto?
    Grazie

    Reply
    1. Giuseppe

      Grazie per il commento Davide.
      La mia affermazione si riferiva agli snippet di codice che ho riportato nell’articolo. Ho sviluppato questa demo solo a scopo dimostrativo, per far capire il meccanismo di innesco e gestione di eventi personalizzati, durante il ciclo di vita di una chiamata Http.
      Come dicevo nell’articolo, trattandosi di sviluppo event-driven, poichè un Interceptor “sporca” di fatto quello che è il naturale flusso Request-Response di una servlet, è facile incappare in degli errori dovuti a bug sul codice, eccezioni non gestite, eventi non presi in considerazione o altro.

      Come da specifica, un Interceptor può essere utilizzato per operazioni di authorization-check (quello che ho fatto io nell’articolo), ma anche per evitare azioni ripetitive in ogni parte dell’applicazione come configurazioni di locale oppure modifca del tema. Insomma tutto ciò che può essere “innescato” prima, dopo e al completamento di una richiesta Http standard.

      Non ci sono controindicazioni sul suo utilizzo in ambito autenticazione, ma se vuoi qualcosa di rodato, già testato e appunto più affidabile puoi riusare quello che qualcuno ha sicuramente fatto prima di me o di te. A questo proposito ti consiglio di dare uno sguardo alla gestione dell’autenticazione con AuthenticationManagerBuilder di Spring Security qui:
      Spring AuthenticationManagerBuilder
      Ti permette di mettere su, con poche righe di codice e utilizzando una suite di oggetti out-of-the-box, una gestione dell’autenticazione basata su LDAP, Active Directory, Utenti e Gruppi su DB o semplicemente su oggetti in memoria.
      Spero di esserti stato utile.
      Giuseppe

Leave a Reply

Your email address will not be published.