Los filtros son uno de los pilares básicos de Java en aplicaciones Web, pero su configuración es muy primaria. En este tutorial te enseñamos cómo emplear Spring para enriquecer tus filtros.
0. Índice de Contenidos
- 1. Introducción
- 2. Entorno
- 3. Filtros
- 4. Filtro de Ejemplo
- 5. DelegatingFilterProxy de Spring
- 6. Llamando a nuestro filtro a través de DelegatingFilterProxy de Spring
- 7. Alterando nuestro Filtro para configurarlo con Spring
- 8. Conclusiones
1. Introducción
Los mecanismos básicos que emplea Java para poder dar servicio en aplicaciones web están especificados en javax.servlet. Estos mecanismos son empleados por todos los frameworks utilizados hoy en día, aunque de una forma más o menos oculta para el desarrollador, dependiendo de la evolución y orientación del framework. Básicamente son 3:
- Servlets: que contienen la lógica que se aplica al recibir una petición HTTP.
- Listeners: que son unas clases que el contenedor de servlet invoca cuando sucenden los eventos de arranque y parada del contenedor.
- Filtros: que se aplican a peticiones HTTP antes o después de que hayan sido atendidas por los servlets, sin que estos sean conscientes de su aplicacion (AOP o programación orientada a aspectos)
Estos elementos son configurados dentro del fichero WEB-INF/web.xml de descripción del contexto de la aplicación según se especifica en el estandard de Java para las aplicaciones Web en los Web Archive (.war), y suponen los pilares básicos sobre los que se implementan los frameworks Web actuales, como pueden ser Spring MVC o Struts2 entre otros muchos.
En este tutorial vamos a centrarnos en los filtros y en cómo poder aprovechar la potencia del framework Spring para su configuración, tanto de comportamiento como de dependencias.
2. Entorno
Para realizar este tutorial se ha empleado el siguiente entorno de desarrollo:
- Hardware: Acer Aspire One 753 (2 Ghz Intel Celeron, 4GB DDR2)
- Sistema Operativo: Ubuntu 15.10
- Java 7, Maven 3, Tomcat 7, Eclipse Mars 2.
El código fuente resultante puede ser descargado en este repositorio de GitHub:
https://github.com/4lberto/simpleWebAppWithDelegatingFilterProxy
3. Filtros
Los filtros son clases de Java que implementan la interfaz javax.servlet.Filter, y cuya misión es interceptar las peticiones antes de que lleguen a los servlets y después, para realizar ciertas operaciones.
¿Para qué se pueden utilizar los filtros? Para un buen número de tareas:
- Control de Seguridad.
- Log de acceso.
- Compresión.
- Preparación de datos para los servlets.
- Encriptación.
- …
La implementacion de la interfaz javax.servlet.Filter tiene mucho que ver con su ciclo de vida. Tiene tres métodos:
- init: cuando se recibe la primera petición para el filtro, se llama a este método y se le pasan los parámetros de configuración y el contexto del servlet. Aquí es donde podemos cambiar el comportamiento del filtro.
- doFilter: cuando se reciben peticiones y ya se ha pasado por init, se invoca directamente a doFilter, que es el que contiene la lógica de lo que hace el filtro. Recibe por parámetro el request y el response, además de la cadena de filtros para pasar la cadena al siguiente filtro.
- destroy: cuando se decide «apagar» el filtro se llama a este método. Sucede en el cierre de la aplicación al hacer un shutdown por ejemplo.
Así, que implementar un Filtro no será otra cosa que implementar estos métodos de la interfaz javax.servlet.Filter
Una de las cosas más interesantes de los filtros es que los servlets no son conscientes de ellos: cuando una petición llega a un Servlet, éste no sabe si previamente esa petición ha sido interceptada por un filtro o no. Esto permite tener muy desacoplado el comportamiento y por tanto favorece la reutilización y el diseño es más claro.
Otro aspecto importante es que los filtros son encadenables, es decir, si la configuración así lo dicta, después de aplicar un filtro, se puede aplicar otro, y así sucesivamente hasta que llega al servlet final o se cancela la petición.
Vamos a ver un ejemplo de Filtro y cómo podemos manejarlo desde Spring con DelegatingFilterProxy.
4. Filtro de Ejemplo
Como hemos visto, los filtros se pueden utilizar para multitud de tareas. Para ilustrar con un ejemplo este tutorial vamos a hacer un filtro muy sencillo de «autenticación» basada en token. Quizá ni siquiera sea eso en realidad :). Te explico el comportamiento:
- Comprueba a ver si la petición (request) contiene un parámetro llamado «token» con un valor determinado:
- Si no tiene el token o lo tiene pero no coincide, cancela la petición antes de llegar al servlet y muestra un mensaje de no autorizado al usuario.
- Si contiene el token y coincide con el configurado, pasa la ejecución al token.
Es bastante sencillo, pero a la vez ilustrativo de la utilidad de los filtros y la independencia de los Servlet. El Servlet no sabe nada de la existencia del filtro. Sólo sabe que no tiene que ocuparse de autenticar nada. De hecho, si no usásemos filtros, esta comprobación de token la tendríamos que haber incluído en cada Servlet, lo cual haría el código más engorroso y pondría en duda el principio de single responsability de los patrones SOLID.
¿Cómo implementamos un filtro así? Pues tendremos que:
- Crear una clase que implemente javax.servlet.Filter-
- Que en su método init reciba, en la configuración, un parámetro con el valor del token que esperamos.
- Que en el doFilter compruebe a ver si en el request está el parámetro y lo compare con el valor configurado.
- Que si es todo correcto, que continúe la cadena de llamadas al siguiente filtro/servlet.
- Que si hay fallo, cancele la request y le informe al usuario a través de un mensaje en el response.
Más o menos esto es lo que hace la siguiente implementación:
package com.adictos.filters; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class SimpleAuthenticationFilter implements Filter { private static final String TOKEN_PARAMETER_KEY = "token"; private static final String TOKEN_CONFIG_KEY = "expectedToken"; private String expectedToken; public void destroy() { } public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { final String tokenFromRequest = request.getParameter(TOKEN_PARAMETER_KEY); if (expectedToken.equalsIgnoreCase(tokenFromRequest)) { chain.doFilter(request, response); } else { response.reset(); response.getWriter().println("Error en la autentiación: token no presente"); response.getWriter().flush(); return; } } public void init(final FilterConfig config) throws ServletException { this.expectedToken = config.getInitParameter(TOKEN_CONFIG_KEY); System.out.println("Filtro " + config.getFilterName() + " configurado con token:" + this.expectedToken); } }
Si nos fijamos en el método init, el token se configura a través de los parámetros que se reciben en el FilterConfig, que se establece en el web.xml al declarar el filtro:
<filter> <filter-name>SimpleAuthenticationFilter</filter-name> <filter-class>com.adictos.filters.SimpleAuthenticationFilter</filter-class> <init-param> <param-name>expectedToken</param-name> <param-value>123456</param-value> </init-param> </filter>
Le estamos indicando que el token que usamos para validar es «123456». Vale, es bastante burdo, pero únicamente es un ejemplo de qué puede hacer un filtro :).
Adicionalmente podemos (y debemos) indicar que el filtro se aplicará cuando se hagan peticiones a un servlet:
<filter-mapping> <filter-name>SimpleAuthenticationFilter</filter-name> <servlet-name>servlet1</servlet-name> </filter-mapping>
Una vez tenemos todo configurado (incluyendo el servlet), podemos ver el resultado corriendo la aplicación Web en un contenedor de servlets. En este caso usaré Tomcat 7 y lo lanzaré desde Eclipse.
Si accedemos a la URL: http://localhost:8080/simpleWebAppWithFilter/servlet1 nos dará este resultado por faltar el token:
Si incluimos en la URL el token en un parámetro obtendremos el resultado deseado: http://localhost:8080/simpleWebAppWithFilter/servlet1?token=123456
5. DelegatingFilterProxy de Spring
Si tienes algo de experiencia con Spring, o al menos has escuchado algo de sus proyectos, sabrás que cuentan con soluciones prácticamente para cualquier casuística que nos podamos encontrar en el desarrollo de una aplicación un poco seria. Y los filtros no son menos.
La declaración del filtro en el web.xml es el mecanismo estándard de configuración del filtro, pero desde luego que no permite tantas posiblidades como podríamos desear: ¿Por qué no poder configurar el filtro como cualquier otro bean de Spring?
Para ello está el DelegatingFilterProxy, que permite traspasar la gestión de los filtros a Spring, gozando así de la potencia de este framework.
DelegatingFilterProxy no es más que un filtro que provee Spring y que le permite hacer de nexo de unión entre el clásico web.xml y la configuración de contexto de Spring en donde podremos declarar los filtros como beans cualquiera. Cuando DelegatingFilterProxy recibe una petición (init, doFilter,destroy), la traspasa al filtro relacionado en Spring directamente.
¿Qué ventaja tiene? Una muy grande: que el filtro destino está en el contexto de Spring y por tanto puede disfrutar de todas las ventajas, como el control y la configuración de dependencias, mucho más potente que la que se puede hacer en el web.xml.
Si alguna vez has trabajado por ejemplo con Spring Security te sonará seguro DelegatinFilterProxy, porque lo utiliza para crear el filtro de control de acceso. Aquí te lo explica mi compañero David Gómez. El código del filtro de Spring Security en el web.xml es el siguiente:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Como puedes ver, DelegatingFilterProxy implementa la interfaz Filter (entre otras más específicas de Spring), ya que es un filtro como otro cualquiera, sólo que se basa en el nombre que le hemos dado para buscar un bean con ese nombre que será un filtro declarado en Spring. De este modo, en la configuración de Spring, al emplear Spring Security, hay un bean llamado springSecurityFilterChain que es al que al final pasa el control del DelegatingFilterProxy.
Así, el contenedor de Servlets (Tomcat en nuestro ejemplo), se comunicará con el el DelegatingFilterProxy, que se ocupará de traspasar estas llamadas al filtro que está en el contexto de Spring. Sería algo así:
6. Llamando a nuestro Filtro a través de DelegatingFilterProxy de Spring
Ya sabemos más o menos la teoría de DelegatingFilterProxy:
- Se introduce en el web.xml y se le da un nombre similar al que le daremos al bean de Spring de nuestro filtro original en el applicationContext.xml.
- Tenemos que declarar nuestro filtro como un bean de Spring.
- Las llamadas que reciba Delegatin Filter Proxy pasarán a nuestro filtro.
Vamos a comenzar por el segundo paso, que condiciona al primero: configurar en Spring nuestro filtro como un bean.
Originalmente ya tenía incluido Spring en el proyecto Web. Es muy sencillo: se introduce el «listener» en el fichero web.xml (otro elemento de la interfaz java.servlet que se encarga de activarse cuando se arranca y para el contendor de servlets). Este listener se ocupa de buscar un fichero de configuración XML que le indicamos como parámetro de contexto en el web.xml también (aunque ahora es común usar anotaciones y no XML) y así configurar el contexto de Spring con esa información.
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:application-context.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
Con esto logramos que el contexto de spring se lea del fichero application-context.xml que he ubicado en /src/main/resources en mi proyecto Maven. El contenido es el siguiente:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:component-scan base-package="com.adictos" /> <bean id="timingService" class="com.adictos.services.TimingService"></bean> <bean id="simpleAuthenticationFilter" class="com.adictos.filters.SimpleAuthenticationFilter"></bean> </beans>
Puedes ver en el último lugar la declaración del simpleAuthenticationFilter, con la «s» en minúscula para diferenciarla de la clase, ya que es un bean de Spring.
Por tanto ya tenemos nuestro filtro, que cumple con la interfaz Filter en el contexto de Spring. Sólo queda engancharlo con la aplicación a través del DelegatinFilterProxy.
Simplemente se debe sustituir en el web.xml el filtro anterior por el nuevo de Spring llamado org.springframework.web.filter.DelegatingFilterProxy. Pero ¡atención! Se debe dar un «filter-name» adecuado, que no es otro que el nombre del bean con el que hemos designado al bean del filtro en Spring: simpleAuthenticationFilter.
Además continuamos configurando el parámetro inicial «expectedToken» con el valor 123456 en el web.xml.
Pero ahora hay un nuevo parámetro inicial, que no es de nuesto filtro, sino del filtro DelegatinfFilterProxy. Tiene mucho que ver con el ciclo de vida de un filtro que hemos visto antes en la introducción.
Si no indicamos ningún parámetro al filtro DelegatingFilterProxy, entonces Spring se ocupará de manejar el ciclo de vida del filtro al cual envía las peticones (simpleAuthenticationFilter). ¿Qué quiere decir esto? Que Spring no llama al init ni destroy en ningún momento, así que no se pueden configurar a través de init como lo estábamos haciendo hasta ahora. Hay dos soluciones:
- 1. Hacer que sea el contenedor de servlets el que llame a init y destroy cuando proceda, indicándolo con el parámetro targetFilterLifecycle.
- 2. Configurar el filtro a través de las inyección de dependencias de Spring y no a través de init.
De momento vamos a seguir la opción 1, que es indicarle con el parámetro targetFilterLifecycle a Spring que el contenedor de Servlets (Tomcat en este caso) va a llamar a init y destroy, y que por tanto, hará caso a los parámetros de configuración que le indicamos en el web.xml
Así pues el web.xml, en lo que afecta al filtro quedará de este modo:
<filter> <filter-name>simpleAuthenticationFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>expectedToken</param-name> <param-value>123456</param-value> </init-param> </filter> <filter-mapping> <filter-name>simpleAuthenticationFilter</filter-name> <servlet-name>servlet1</servlet-name> </filter-mapping>
Si probamos la aplicación funciona correctamente
¿Y si queremos aprovechar la potencia de Spring para hacer una configuración más avanzada de nuestro filtro, olvidándonos del web.xml? Vamos a verlo a continuación:
7. Alterando nuestro Filtro para configurarlo con Spring
El valor 123456 que hemos establecido para el «expectedToken» en el web.xml se configura en el filtro a través del método init, y es Tomcat el que le pasa un objeto de configuración con esta información para que sea leída en el filtro. En código:
public void init(final FilterConfig config) throws ServletException { this.expectedToken = config.getInitParameter(TOKEN_CONFIG_KEY); }
Efectivamente, en el FilterConfig están mapeados los valores del web.xml, además de otra información del contexto del Servlet que ahora no estamos utilizando.
¿Cómo cambiarlo por configuración de Spring?
Se me ocurre que podemos crear un constructor a nuestra clase SimpleAuthenticationFilter donde se le pase por parámetro un String con el valor que le demos a expectedToken. Posteriormente en el fichero application-context.xml especificaremos este valor como constructor:
public SimpleAuthenticationFilter(String expectedToken) { super(); this.expectedToken = expectedToken; }
Y en el fichero Spring indicamos por constructor este valor:
<bean id="simpleAuthenticationFilter" class="com.adictos.filters.SimpleAuthenticationFilter"> <constructor-arg value="123456" type="java.lang.String"></constructor-arg> </bean>
Así que ahora podemos quitar el parámetro de inicio del web.xml y como ya no tendríamos que inyectar nada, también el targetFilterLifecycle indica que el contenedor de Servlets tiene que llamar a init porque no nos hace falta (incluso podemos quitar -mejor vaciar porque lo requiere la interfaz- el método init).
<filter> <filter-name>simpleAuthenticationFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>simpleAuthenticationFilter</filter-name> <servlet-name>servlet1</servlet-name> </filter-mapping>
El funcionamiento es exactamente igual que antes, solo que ahora tenemos la flexibilidad de Spring para configurar mínimamente nuestro filtro.
Y como ya hemos abierto la «caja de Pandora» de Spring, vamos a inyectar en nuestro filtro un servicio que nos da la hora actual formateada. Nuevamente es algo muy sencillo, pero es para ilustrar con algún ejemplo.
El servicio es el siguiente:
public class TimingService { public String getCurrentTime() { final SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy hh:mm:ss"); return sdf.format(new Date()); } }
Lo podríamos haber anotado, pero si te has fijado bien antes, ya lo teníamos configurado en el application-context.xml de Spring:
<bean id="timingService" class="com.adictos.services.TimingService"></bean>
Alteramos nuestro código del filtro para introducir un miembro nuevo que albergue la referencia al nuevo TimingService. Esta vez haremos uso de la anotación @Autowired en nuestro filtro:
@Autowired private TimingService timingService;
Y lo usamos para dar la fecha y hora en el mensaje si hay fallo en el filtro:
response.getWriter().println("Error en la autentiación: token no presente. Fecha y hora:" + timingService.getCurrentTime());
Y si probamos la aplicación, efectivamente tenemos el resultado esperado cuando no indicamos el token 123456 como parámetro. Ahora nos muestra la hora:
El restultado final lo puedes descargar en: https://github.com/4lberto/simpleWebAppWithDelegatingFilterProxy
8. Conclusiones
En este tutorial hemos visto el mecanismo que tiene Spring para poder integrar los filtros de javax.servlet dentro de su contexto y así poder acceder a todas las ventajas de este conocido framework de aplicaciones empresariales.
Muy bueno. Me ha sido de gran ayuda.
Gracias!!!!!
Muy claro y bien explicado. Muchas gracias.
Excelente ejemplo gracias, puedes compartir el codigo fuente