Estableciendo filtros en Spring June 13, 2019

En esta entrada voy a hablar de como implementar filtros en Spring. Los filtros son de los que se pueden establecer cuando se recibe una petición HTTP. Es decir, suponiendo que tenemos un programa escuchando en unas URI, poder especificar que deseamos ejecutar algo antes de que las peticiones sea procesadas por el controlador.

Esto es muy útil si queremos que todas las peticiones cumplan un requisito, por ejemplo incluir una cabecera especifica.

Para entender como funcionan los filtros en Spring he realizado un programa que iré explicando poco a poco.

El código fuente del programa lo tenéis en mi página de GITHUB

Empezare mostrando el controlador para peticiones REST que esta en la clase PrincipalController.java. Este será el encargado de gestionar todas las peticiones.

@RestController
public class PrincipalController {
	@Autowired
	SillyLog sillyLog;
	
	@GetMapping("*")
	public String entryOther(HttpServletRequest request,HttpServletResponse response)
	{	
		sillyLog.debug("In entryOther");
		if (response.getHeader("PROFE")!=null)
			sillyLog.debug("Header contains PROFE: "+response.getHeader("PROFE"));
		if (response.getHeader("CAKE")!=null)
			sillyLog.debug("Header contains CAKE: "+response.getHeader("CAKE"));
		return "returning by function entryOther\r\n"+
				sillyLog.getMessage();
	}
	@GetMapping(value={"/","one"})
	public String entryOne(HttpServletRequest request,HttpServletResponse response	)
	{
		sillyLog.debug("In entryOne");
		if (response.getHeader("PROFE")!=null)
		{
			sillyLog.debug("Header contains PROFE: "+response.getHeader("PROFE"));
			return entryTwo(response);				
		}
		return "returning by function entryOne\r\n"+
				sillyLog.getMessage();
	}
	@GetMapping("two")
	public String entryTwo(HttpServletResponse response)
	{
		sillyLog.debug("In entryTwo");
		if (response.getHeader("PROFE")!=null)
			sillyLog.debug("Header contains PROFE: "+response.getHeader("PROFE"));
		return "returning by function entryTwo\r\n"+
				sillyLog.getMessage();
	}
	@GetMapping("three")
	public String entryThree()
	{
		sillyLog.debug("In entryThree");
		return "returning by function entryThree\n"+
				sillyLog.getMessage();
	}
	@GetMapping("redirected")
	public String entryRedirect(HttpServletRequest request)
	{
		sillyLog.debug("In redirected");
		return "returning by function entryRedirect\n"+
				sillyLog.getMessage();
	}
}

En la función entryOther se capturaran todas las peticiones tipo GET que vayan a alguna URI que no tengamos definidas explícitamente. En la función entryOne se procesaran las peticiones tipo GET que vayan a la URL http://localhost:8080/one o http://localhost:8080/ y así sucesivamente.

La clase sillyLog es una clase donde simplemente iremos añadiendo líneas de log para luego devolverlas en el body de la respuesta, de tal manera que podremos ver por donde ha pasado nuestra petición.

En esta aplicación se definen tres filtros: MyFilter.java ,OtherFilter.java y CakesFilter.java. El primero tiene preferencia sobre el segundo, por estar así establecido en el parámetro de la etiqueta @Order. Del tercero hablo al final del articulo.

En el fichero MyFilter.java definimos nuestro primer filtro.

@Component
@Order(1)
public class MyFilter implements Filter{
	@Autowired
	SillyLog sillyLog;
	
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)	throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse  myResponse= (HttpServletResponse) response;
		sillyLog.debug("Filter: URL"
				+ " called: "+httpRequest.getRequestURL().toString());
		
		if (httpRequest.getRequestURL().toString().endsWith("/one"))	{			
			myResponse.addHeader("PROFE", "FILTERED");						
			chain.doFilter(httpRequest, myResponse);
			return;
		}
        if (httpRequest.getRequestURL().toString().endsWith("/none"))	{   
            myResponse.setStatus(HttpStatus.BAD_GATEWAY.value());
		    myResponse.getOutputStream().flush();
		    myResponse.getOutputStream().println("-- I don't have any to tell you --");
            return; // No hago nada.
        }
		if (httpRequest.getRequestURL().toString().endsWith("/redirect"))	{			
			myResponse.addHeader("PROFE", "REDIRECTED");
			myResponse.sendRedirect("redirected");
			chain.doFilter(httpRequest, myResponse);
			return;
		}
		if (httpRequest.getRequestURL().toString().endsWith("/cancel"))	{			
			myResponse.addHeader("PROFE", "CANCEL");
			myResponse.setStatus(HttpStatus.BAD_REQUEST.value());
			myResponse.getOutputStream().flush();
			myResponse.getOutputStream().println("-- Output by filter error --");
			chain.doFilter(httpRequest, myResponse);
			return;
		}
		chain.doFilter(request, response);
	}
}

La clase OtherFilter es más simple:

@Component
@Order(2)
public class OtherFilter implements Filter{
	@Autowired
	SillyLog sillyLog;
	
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest httpRequest= (HttpServletRequest) request;
		HttpServletResponse  myResponse= (HttpServletResponse) response;
		sillyLog.debug("OtherFilter: URL"
				+ " called: "+httpRequest.getRequestURL().toString());
		if (myResponse.getHeader("PROFE")!=null)
		{
			sillyLog.debug("OtherFilter: Header contains PROFE: "+myResponse.getHeader("PROFE"));
		}
		chain.doFilter(request, response);
	}

}

Lo primero que tenemos que hacer para definir un filtro general es etiquetar la clase con @Component . Después deberemos implementar el interface Filter . También podríamos extender de la clase OncePerRequestFilter la cual implementa el interface Filter y añade ciertas funcionalidades para que un filtro solo se ejecute una vez por ejecución. En este ejemplo vamos a simplificarlo al máximo y directamente implementaremos el interface Filter.

El interface Filter tiene tres funciones.

Como he comentado anteriormente, la etiqueta @Order nos permitirá especificar el orden en que los filtros serán ejecutados. En este caso, este filtro tendrá el valor 1 y el siguiente tendrá el valor 2, por lo cual MyFilter se ejecutara antes que OtherFilter.

La clase MyFilter realiza diferentes acciones según la URL llamada. La clase OtherFiltersolamente añade un log cuando pasa por ella.

En el código del ejemplo nosotros solo utilizamos la función doFilter. En ella, lo primero, convertimos la clase ServletResponse a HttpServletResponse y la clase ServletRequest a HttpServletRequest. Esto es necesario para poder acceder a ciertas propiedades de los objetos que de otra manera no estarían disponibles.

Voy a explicar paso a paso los diferentes casos contemplados en la clase MyFilter, dependiendo de la URL invocada.

La primera línea es devuelta por la función entryTwo. A continuación se muestran los logs añadidos.

Lo mejor es mirar el código fuente si no se tiene claro de donde salen tantas líneas 😉

Esta es la salida que tendremos si realizamos una petición con curl:

> curl -s http://localhost:8080/redirect

Efectivamente, no hay salida. ¿Por qué?. Pues porque hemos incluido una directiva redirected y curl por defecto no sigue esas directivas, con lo cual simplemente no muestra nada.

Veamos, que esta pasando añadiéndole a curl el parámetro -v (verbose)

> curl -v -s http://localhost:8080/redirect
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /redirect HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.60.0
> Accept: */*
>
< HTTP/1.1 302
< PROFE: REDIRECTED
< Location: http://localhost:8080/redirected
< Content-Length: 0
< Date: Thu, 13 Jun 2019 13:57:44 GMT
<
* Connection #0 to host localhost left intact

Esto es otra cosa, ¿verdad?. Ahora muestra en la cabecera nuestro valor para PROFE Y vemos la orden de redirigir a http://localhost:8080/redirected. Observar que el código HTTP es 302, que es redirect.

Sí le decimos a curl que siga la redirección, pasándole el parámetro -L, veremos lo que esperábamos.

    > curl -L -s http://localhost:8080/redirect
    returning by function entryRedirect
    SillyLog: dcfc8b09-84a4-40a1-a2d6-43340abdf50c/1 Filter: URL called: http://localhost:8080/redirected
    SillyLog: dcfc8b09-84a4-40a1-a2d6-43340abdf50c/2 OtherFilter: URL called: http://localhost:8080/redirected
    SillyLog: dcfc8b09-84a4-40a1-a2d6-43340abdf50c/3 In redirected

Bueno, casi lo que esperábamos. Obsérvese que ha habido dos peticiones HTTP a nuestro servicio y solo se muestra los datos de la segunda.

Observar que el cuerpo añadido en el filtro es anterior a lo devuelto por el controlador.

Para especificar que un filtro solo sea activo para ciertas URL, hay que registrarlo explícitamente y no marcar la clase con la etiqueta @Component. En el proyecto de ejemplo en la clase FiltrosApplication vemos la función donde se añade un filtro:

@Bean
    public FilterRegistrationBean<CakesFilter> cakesFilter()
    {
        FilterRegistrationBean<CakesFilter> registrationBean = new FilterRegistrationBean<>();              
        registrationBean.setFilter(new CakesFilter());
        registrationBean.addUrlPatterns("/cakes/*");
        return registrationBean;
    }

La clase CakesFilter es la siguiente:

@Order(3)
public class CakesFilter implements Filter{     
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {        
        HttpServletResponse  myResponse= (HttpServletResponse) response;
        myResponse.addHeader("CAKE", "EATEN");  
        chain.doFilter(request, response);
    }
}

Al hacer una llamada a una url que empiece por /cakes/* veremos como se ejecuta el ultimo filtro.

> curl  -s http://localhost:8080/cakes
returning by function entryOther
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/1 Filter: URL called: http://localhost:8080/cakes
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/2 OtherFilter: URL called: http://localhost:8080/cakes
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/3 In entryOther
SillyLog: 41e2c9b9-f8d2-42cc-a017-08ea6089e646/4 Header contains CAKE: EATEN

Por la manera en que tiene Spring de gestionar sus variables de contexto, no es posible inyectar el objeto SillyLog con un @Autowired . Si lo inyectamos veremos como la variable tiene el valor null

Y con esto doy por finalizada esta entrada ¡¡ Hasta la próxima !!