Mostrando las entradas con la etiqueta apache. Mostrar todas las entradas
Mostrando las entradas con la etiqueta apache. Mostrar todas las entradas

domingo, 2 de junio de 2013

Filtros de Tomcat - Parte II

En el post anterior (Filtros de Tomcat - Parte I) expliqué los fundamentos de los filtros en Tomcat. En este segunda parte mostraré un ejemplo práctico de cómo utilizar un filtro.

El código fuente de referencia está disponible aquí.

1. Ejemplo Práctico: Filtro de Autenticación
El objetivo del filtro es asegurar que las páginas sólo estén disponibles para usuarios que han superado exitosamente el proceso de autenticación. Sin un filtro, este requerimiento implica la intervención de todas las páginas .jsp de la aplicación (asumamos por un momento que no se está trabajando con ningún framework específico que ya provea esta funcionalidad).

Para efectos del ejemplo, consideraremos un sitio web muy simple, en adelante "minisitio" (como dicen los abogados), que tiene una página con contenido (index.jsp) y una página de ayuda (ayuda.jsp). La página index.jsp está restringida para los usuarios autenticados correctamente. La página ayuda.jsp no requiere usuarios autenticados para acceder a ella. El flujo típico de autenticación de usuarios es el que se muestra en el siguiente diagrama. Las páginas del lado izquierdo requieren usuarios autenticados. Las páginas del lado derecho no.
2. Autenticación
Hay múltiples maneras de resolver la autenticación de usuarios. Para efectos de este ejemplo, la autenticación exitosa será registrada utilizando las variables de sesión del contexto. En términos simples, si un usuario se autenticó correctamente, se marcará la variable "logged" de la sessión con el valor "true".

Considerando el diagrama anterior, el flujo es como sigue:
  1. Usuario ingresa a la página login.jsp, ingresa su usuario y password y pulsa el botón Ingresar.
  2. Los datos son enviados a la página checkLogin.jsp que valida si el usuario es válido contra algún sistema (base de datos, archivo, etc.)
  3. Si no es válido, reenvía al usuario al paso 1 con un mensaje descriptivo del error. 
  4. Si es válido, se registra el valor "true" en la variable "logged" de la sesión. 
  5. El usuario puede acceder a la página de contenido index.jsp
  6. Cuando el usuario quiere salir del sistema, es enviado a la página logout.jsp que invalida la sesión.
  7. El usuario es redirigido a la página login.jsp
3. Validación sin Filtro
Sin el uso de un filtro, la validación de los usuarios autenticados en las páginas restringidas implicaría agregar el código para validar la información de la sesión en cada página. Si bien es cierto esto se puede hacer reutilizando código por medio de includes, funciones, bibliotecas, etc., no es una estrategia que permita crecer en el tiempo. Un ejemplo simple de este enfoque es que la responsabilidad de agregar la validación queda en manos del desarrollador. Si el desarrollador olvida incorporar el código que valida la sesión, la página quedará expuesta a usuarios no autenticados.

4. Validación con Filtro
Veamos ahora cómo implementar un filtro para validar el acceso a las páginas restringidas del minisitio descrito anteriormente.

4.1 Configuración del Filtro
Considerando el flujo de autenticación descrito previamente (algo así como los requerimientos), hay algunas páginas que deben tener autenticación y otras que no. La página ayuda.jsp es, por definición, una página que no requiere usuarios autenticados, sin embargo, las páginas login.jsp, checkLogin.jsp y logout.jsp tampoco requieren usuarios autenticados. No tiene mucho sentido exigir un usuario autenticado para la página de login por ejemplo. Lo mismo sucede con las otras. Por otra parte, cuando el filtro detecte el acceso de un usuario no autenticado a una página restringida, debe saber a qué página debe enviar al usuario para que se autentique. En el caso del minisitio, la página es login.jsp, sin embargo, podría ser necesario modificar esta página dinámicamente.

Para cumplir con lo anterior, se considera la definición de dos parámetros (exclude y loginPage) en la configuración del filtro para permitir modificar este comportamiento dinámicamente. La configuración, será como sigue en el archivo web.xml.

  <filter>
    <filter-name>LoginFilter</filter-name>
    <filter-class>cl.abertini.filters.LoginFilter</filter-class>
    <init-param>
        <param-name>exclude</param-name>
        <param-value>/login.jsp ... /ayuda.jsp</param-value>
    </init-param>
    <init-param>
        <param-name>loginPage</param-name>
        <param-value>/login.jsp</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>LoginFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>


4.2 Construcción del Filtro

Como se mencionó en la primera parte, el filtro se construye heredando de la clase javax.servlet.Filter y se deben implementar los métodos siguientes init(...), doFilter(...) y destroy(). A continuación se describe la implementación realizada en el contexto del ejemplo.
  • init(FilterConfig filterConfig)

    El método, básicamente, lee la configuración del archivo web.xml y rescata la información de los parámetros exclude (páginas que no requieren usuarios autenticados) y loginPage (la página que permite a un usuario autenticarse).

    La configuración se almacena en las variables sExclude y sLoginPage de la clase para ser utilizadas en el método doFilter(...).
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

    Este es el método principal y que será ejecutado cada vez que se acceda a una página del contexto.

    En términos simples, primero se valida que la página a la que se está accediendo no sea una página excluida. En caso de serlo, se pasa el control al siguiente filtro de la cadena (secuencia) por medio de la instrucción chain.doFilter( request, response ).

    En caso de no ser una página excluida, se valida si el atributo "logged" está en la sessión. Para efectos del ejemplo, sólo se valida que exista y no necesariamente que tenga el valor "true".

    Si el atributo no existe, se redirige la respuesta a la página configurada como loginPage. Notar que en este caso, no se traspasa el procesamiento a ningún filtro de la cadena una vez que se reenvió al usuario a la página indicada.

    En caso contrario, es decir, que el atributo "logged" existe, simplemente se traspasa el control al siguiente filtro como se explicó anteriormente.
  • destroy()

    En este ejemplo, dado que la información que administra el filtro (parámetros exclude y loginPage) no consumen muchos recursos, no se realiza nada. Si el filtro que se esté implementado, requiriera administrar grandes cantidades de memoria y/o estructuras de datos complejas y/o cachés o similares, entonces, en este método se debieran implementar los procedimientos para asegurar una correcta destrucción (y almacenamiento tal vez) de la información.
5. Pruebas
Para probar el código anterior, se requiere tener instalado Tomcat 6.X ó superior. Se debe habilitar el contexto en el directorio webapps y levantar el servidor. Una vez levantado, se debiera poder ver el sitio en la siguiente URL:

http://localhost:8080/testSite/login.jsp

Para probar que el filtro está operativo, se puede intentar acceder a la página restringida, con la siguiente URL:

http://localhost:8080/testSite/index.jsp

Si es que no se ha realizado el proceso de autenticación, el filtro debiera bloquear el acceso y reenviar el browser a la página de login. Por el contrario, si se accede a la página de ayuda, no debiera haber problemas ya que está definida como una página sin acceso restringido. Para acceder a esta página, se debe utilizar la siguiente URL:

http://localhost:8080/testSite/ayuda.jsp

Para probar la autenticación, se debe usar la palabra "prueba" como usuario y password en la página de login. Si se ingresan los datos correctos, se debiera poder acceder a la página index.jsp sin problemas.

6. Más Filtros y Extensiones
La distribución estándar de Tomcat 7 incluye varios filtros. En términos generales, los filtros provistos proveen funcionalidades destinadas a mejorar la seguridad del sitio (XSS, CSRF), algunos para depuración y otros para manipular aspectos del HTML entregado al cliente (encoding por ejemplo).

En la siguiente página de Oracle hay una muy buena descripción de la infraestructura de filtros y algunos ejemplos interesantes.

The Essentials of Filters

De todos los ejemplos que he visto al respecto, el siguiente me parece el mejor para demostrar las capacidades de los filtros. El filtro descrito permite reducir el tamaño del HTML entregado al cliente eliminando algunas secuencias como espacios, líneas en blanco, etc. Es interesante porque muestra un truco para poder "capturar" la respuesta (HTML) que va a ser entregada al cliente, como parte del flujo de salida de la cadena (secuencia) de filtros (ver punto 3.2 de la primera parte). Una extensión de este mecanismo sería la construcción de un modelo simple de templates.

Minifying HTML in the Servlet container

7. Conclusiones
La próxima vez que se vaya a implementar una funcionalidad transversal a un sitio, es recomendable analizar si aplica al construcción de un filtro o no. En esta consideración, más allá de las cosas operativas y las precauciones que se pudieran tomar como parte del proceso de desarrollo, la más importante es la posibilidad de asegurar que ciertas funcionalidades se apliquen sin necesidad de depender de la "buena voluntad" de los desarrolladores. Esto, a la larga, no sólo simplifica el proceso si no que, además, asegura que los desarrolladores se focalicen en el desarrollo de las páginas y sus objetivos. Las condiciones higiénicas asociadas ya van a estar implementadas.

sábado, 25 de mayo de 2013

Filtros de Tomcat - Parte I

Supongamos por un momento que es necesario cortar un árbol y se poseen las siguientes herramientas: un Hacha y un Serrucho.

¿Cuál herramienta es la más eficiente?

La mayoría de las personas considerarán el serrucho como la más eficiente. ¿Por qué? Básicamente porque todos conocemos las capacidades de un serrucho y podemos compararla con el hacha. Supongamos ahora que no conocemos ninguna de las herramientas y debemos elegir. La elección no es tan simple porque no conocemos las capacidades de cada una. El sentido común no nos permitiría hacer una elección correcta tampoco.

Lo anterior es un ejemplo muy simple de lo importante que es conocer (y tener) las herramientas adecuadas para realizar una tarea.

Hace algunas semanas atrás, un cliente solicitó una modificación a uno de los productos de la empresa en la que trabajo. La modificación implicaba modificar todas las páginas .jsp de la aplicación y, obviamente, había un plazo que cumplir, por lo que, la intervención manual de todas las páginas no era una opción. Había que buscar otra alternativa. ¿La solución? Filtros de Tomcat.  

1. Filtros de Tomcat
El servidor web Apache Tomcat provee una infraestructura denominada Filtros que permite interceptar el flujo de entrada y salida desde y hacia una página (.jsp) e intervenirlo. La gracia es que el filtro se programa una vez y permite realizar la tarea en todas las páginas existentes en el contexto.

La infraestructura de filtros está disponible desde la Servlet API 2.3 y fue mejorada en la versión 2.4, permitiendo que los filtros sean aplicados incluso en el traspaso de las solicitudes entre las páginas (por medio de las directivas forward o include). Los diagramas siguientes muestran cómo operan los filtros en el flujo de procesamiento de una página en las dos versiones.



2. Ejemplos de Filtros
Considerando el flujo anterior, un Filtro se puede construir para realizar diversas tareas. Algunos ejemplos simples son los siguientes:
  • Validaciones de Seguridad. Validar que cada página cumpla una condición determinada, por ejemplo, que el usuario esté autenticado.
  • Compresión de Salidas/Entradas. Comprimir un archivo automáticamente para minimizar el tiempo de descarga.
  • Registro de Eventos. Generar un log de auditoría respecto con la información de los accesos a las páginas de la aplicación.
  • Modificación/Intervención de Salidas/Entradas. Eliminación de caracteres especiales, sobrantes, limpieza de HTML, etc.
La implementación de un filtro implica las siguientes actividades:
  • Construcción de la clase que implementará el filtro con la funcionalidad deseada.
  • Configuración del filtro en Tomcat para que realice su tarea
Obviamente, la construcción del filtro debe realizarse primero que la configuración, sin embargo, voy a describir la configuración primero porque es relevante para entender el "encadenamiento" de los filtros en el procesamiento y la construcción correspondiente.

3. Configuración del Filtro
La configuración del filtro se realiza en el archivo web.xml del contexto (dentro de la carpeta WEB-INF/). Se debe agregar una configuración como la siguiente para cada filtro que se quiera habilitar:

<filter>
  <filter-name>NOMBRE</filter-name>
  <filter-class>CLASE</filter-class>
  <init-param>
    <param-name>PARAM_1</param-name>
    <param-value>VALOR_1</param-value>
  </init-param>

  :
  <init-param>
    <param-name>PARAM_n</param-name>
    <param-value>VALOR_n</param-value>
  </init-param>
</filter>


<filter-mapping>
  <filter-name>NOMBRE</filter-name>
  <url-pattern>PATRON</url-pattern>
</filter-mapping>


3.1 Sección <filter>.
Permite configurar un filtro en el contexto.
  • NOMBRE. Corresponde a un nombre lógico que identifica la acción/objetivo que realiza el filtro. Por ejemplo: LoginFilter, RegExFilter, HitCountFilter, etc.
  • CLASE. Corresponde al nombre de la clase que implementa el filtro. Obviamente, debe ser una clase que esté disponible para el contexto de la aplicación.
  • PARAM_i y VALOR_i. Se pueden agregar parámetros de configuración relevantes para el filtro agregando nodos del tipo <init-param>. Cada nodo contiene un nombre (PARAM_i) y un valor (VALOR_i).
3.2 Sección <filter-mapping>.
Permite configurar a qué páginas se aplicará el filtro.
  • NOMBRE. Corresponde al nombre lógico del filtro que se indicó en la sección anterior. Debe ser el mismo para que aplique correctamente.
  • PATRON. Corresponde al patrón de páginas para las cuales se desea que se aplique el filtro. Un ejemplo es /* que indica que aplica a todas las páginas del contexto.
Un aspecto importante de la configuración en el archivo web.xml es que el orden en el que se declaran los filtros. Por ejemplo, supongamos que tenemos dos filtros: HitCountFilter y LoginFilter. El primero registra el número de accesos a las páginas del sitio y el segundo valida que un usuario esté autenticado. Veamos dos configuraciones posibles en el web.xml:


La configuración a) implica que primero se registrarán los accesos y luego se validará al usuario. La configuración b) implica que primero se validará al usuario y luego se registrarán los accesos.

Lo anterior se denomina encadenamiento de filtros (Filter Chain) y establece el orden en el que se deben aplicar los filtros. Lo importante de esto es que los filtros se aplican en cadena sobre la entrada y, luego, de manera inversa sobre la salida. Considerando el ejemplo a) anterior, se produce la siguiente secuencia:


La entrada (E1) es recibida y procesada por HitCounter. El resultado (E2) es entregado a LoginFilter quien la procesa. El resultado (E3) es entregado a la página.jsp para su procesamiento. A esta altura, la ejecución de la página.jsp es similar a la ejecución que se realizaría si no hubiera filtros configurados, es decir, la página.jsp se programa como una página normal, independiente de los filtros que podrían haber sido configurados previamente. El flujo de salida opera de la misma manera permitiendo realizar acciones sobre el resultado (output) de la página.jsp. Ejemplos de acciones posibles a este nivel son la compresión del HTML (eliminando espacios en blanco) o el reemplazo de contenido por otro utilizando expresiones regulares.

4. Construcción del Filtro 
Un filtro se construye heredando de la clase javax.servlet.Filter y, en él, se deben implementar tres métodos:
  • init(FilterConfig filterConfig).
    Este método se ejecuta cuando el contexto es cargado por el servidor y sólo se ejecuta una vez. El parámetro filterConfig permite acceder a las configuraciones que se hayan realizado para el filtro en el archivo web.xml por medio de la secciones <init-param/>. 
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain).
    Este método se ejecuta cuando se requiere aplicar el filtro a la entrada (request) y a la salida (response). El parámetro chain es el que permite traspasar el control al siguiente filtro en la cadena. En casos específicos, se puede interrumpir el flujo programáticamente. Por ejemplo, si el filtro LoginFilter identifica que el usuario no está autenticado, no es necesario ejecutar la página .jsp correspondiente.
  • destroy(). Este método se ejecuta cuando se baja el contexto y puede/debe ser utilizado para liberar recursos que se hayan utilizado para la operación del filtro. Por ejemplo, cachés, contadores, etc.
En la próxima parte describiré un ejemplo práctico de cómo hacer un filtro de autenticación de usuarios que permita validar, de una manera simple y centralizada, que el acceso a las páginas lo realicen únicamente usuarios que se hayan autenticado correctamente.

lunes, 29 de abril de 2013

Comentar el Código... o la Magia de Ponerse en el Lugar del Otro

En el ámbito del desarrollo de Software, se denomina Código Fuente a una secuencia de instrucciones de computador escritas en un lenguaje comprensible por un ser humano, típicamente, como texto. Haciendo la analogía con el mundo real, el Código Fuente es similar a la descripción de un procedimiento. Por ejemplo, consideremos el siguiente procedimiento de control de acceso:

1. Acérquese a la puerta.
2. Digite su número de identificación.
3. Coloque su mano en el lector biométrico.
4. Espere la validación exitosa.
5. En caso de ser exitosa, abra la puerta.
6. En caso contrario, reingrese su código y comience nuevamente desde el paso número 2.

En el caso del Código Fuente, éste describe (similar a un procedimiento) lo que debe hacer el computador para lograr un objetivo. Está diseñado para facilitar el trabajo de los programadores o desarrolladores de software, que son los profesionales responsables de "traducir" las necesidades de los usuarios en Código Fuente. Una vez que el Código Fuente está listo, es traducido en lenguaje de bajo nivel (lenguaje comprensible por el computador) utilizando un Software denominado Compilador. El compilador actúa como un traductor Inglés-Español, por ejemplo, traduciendo desde el lenguaje procedural (Código Fuente) comprensible por un humano (desarrollador), al lenguaje de máquina comprensible por el computador. El resultado del compilador es lo que se denomina un Ejecutable o Aplicación, y es lo que, finalmente, usan las personas.

En términos generales, el Código Fuente siempre va a estar en fase de Desarrollo y/o Mantención. La fase de Desarrollo corresponde a la fase de construcción inicial de una aplicación, es decir, desde cero. La fase de Mantención corresponde a la fase posterior al término de la aplicación y que, básicamente, se dedica a la tarea de corregir pequeños errores e incorporar funcionalidades nuevas a la misma. En este sentido, es importante tener presente que, a menos que se esté desarrollando una tarea y/o una aplicación muy pequeña, siempre el Código Fuente estará siendo manipulado por varios desarrolladores al mismo tiempo. En el mejor de los casos, desarrolladores en una misma ubicación geográfica. En el peor de los casos, desarrolladores distribuidos globalmente.

En cualquiera de las fases, desarrollar Código Fuente requiere tener capacidad lógica, conocer el lenguaje de programación, ser ordenado, metódico y creativo, aunque suene contradictorio. Y, adicionalmente, exige ser capaz de incorporar Comentarios al Código Fuente que permitan aclarar, declarar, ejemplificar y/o documentar alguna condición particular o específica que no sea evidente para un tercero. Incluso, debiera realizarse en aquellos casos en que no participe nadie más en el desarrollo.

Los Comentarios cobran mayor relevancia cuando se está en un proceso de Mantención. En general, la Mantención implica ser capaz de arreglar y/o mejorar algo sin generar nuevos problemas. Haciendo una analogía con el mundo físico, es como intentar cambiar una rueda con el auto andando. La Mantención es una tarea compleja, en especial, para aquellas aplicaciones que han sido desarrollados hace ya mucho tiempo y/o que han sido mantenidos por variadas personas y, peor aún, para aquellos que no tienen ningún tipo de documentación disponible.

Cuando se realiza mantención de un Código Fuente, un desarrollador debería ser capaz de entender la lógica de la aplicación fácilmente. Cualquier detalle, dependencia y/o vinculación que no sea detectada a tiempo, implicaría de inmediato la posibilidad de incorporar nuevos problemas al intentar hacer una corrección.

La primera manera de evitar esto es, como mencioné antes, incorporando Comentarios al Código Fuente. La segunda manera, y más importante aún, es incorporar Comentarios que realmente hagan sentido. Esto, claramente, es lo más difícil de lograr.

Veamos algunos ejemplos.

1. Tiempo Verbal
Es fundamental comprender que al momento de comentar el código fuente, lo que se requiere es describir lo que hace el código (procedimiento), no lo que "yo he decidido hacer", desde el punto de vista del desarrollador. En este contexto, los comentarios, siguientes son incorrectos.

// Recorro el arreglo
for( int i = 0; i < 10; i++ )
...

// Valido el largo del texto
if( sText != null && sText.length() == 0 )
...

La solución consiste en escribir los comentarios anteriores como sigue (aún cuando siguen siendo incorrectos como se describe en el siguiente punto):

// Recorrer el arreglo
// Validar el largo del texto

2. Comentarios con Sentido
Los comentarios deben incorporar información relevante. El comentario siguiente, no entrega ningún antecedente que un desarrollador con algo de experiencia no pueda identificar de inmediato.

// Recorro el arreglo
for( int i = 0; i < 10; i++ )
...

Ahora bien, si recorrer el arreglo anterior es importante, el comentario se debe reescribir adecuadamente:

// Recorrer el arreglo para determinar la fecha
// de ingreso menor de los documentos elegidos
for( int i = 0; i < 10; i++ )
...

3. Inicialización de Variables
Probablemente, el comentario más inútil y más utilizado por los desarrolladores novatos. Cualquiera de las variantes siguientes son innecesarias.

Form oForm = new Form(); // Inicializo la variable

// Inicializar las variables
String name = "";
String region = "";

// Abro la Conexión con la Base de Datos
Connection oConn = db.GetConnection();

4. Comentar Omisiones Controladas
Durante el desarrollo del Código Fuente, hay condiciones que serán ignoradas. Las razones pueden ser variadas, pero, en aquellos casos en que no son obvias, debe dejarse por explícito la omisión. A continuación, algunos ejemplos.

switch( status) {
  case 1:
    mes = "Encendido";
    break;
  case 0:
    //Fall-Through - Cualquier otro valor
  default:
    mes = "Apagado";
}

En el caso anterior, se deja explícito que, cualquier valor distinto de 1, se considera "Apagado".

try {
  :
  :
}
catch( Exception ex ) {
  // Dummy, no se hace nada
}

En el caso anterior, se deja explícito que no se va a procesar ninguna Excepción ocurrida durante el procesamiento. Si no se incluye el comentario, un desarrollador podría considerar relevante el procesamiento de alguna excepción y romper el flujo normal del programa.

5. Yo Pensé que Tú Pensaste
El uso e inclusión de estructuras de datos, bibliotecas de terceros, etc., es casi una obligación hoy en día. Gracias a fundaciones como Apache y al movimiento OpenSource, no es necesario partir desde cero al momento de realizar un desarrollo, sin embargo, si es necesario documentar las razones por las cuales se realizaron las elecciones correspondientes.

Veamos un ejemplo para el uso de una estructura de datos determinada.

// Inicializo el Hashtable
Hashtable oHT<..., ...> = new Hashtable<..., ...>();

Un programador inexperto, podría considerar que es mejor usar un Hashmap que un Hashtable porque "básicamente proveen las mismas operaciones". El comentario, debe ser explícito para aclarar esto.

// Se usa un Hashtable para evitar problemas de
// concurrencia en el acceso a los datos
Hashtable oHT<..., ...> = new Hashtable<..., ...>();

En el caso de las bibliotecas de terceros, la situación es la misma.

// Recuperar la interfaz del Logger
static Logger log = Logger.getLogger(...);

En este caso, se requiere declarar que se está utilizando una versión de Log4j determinada.

// Se utiliza Log4j 2, para evitar problemas de rendimiento
static Logger log = LogManager.getLogger(...);

En el ejemplo anterior, probablemente el ambiente de desarrollo (IDE) declararía que el método no está presente con otras versiones, sin embargo, es necesario incorporar este tipo de comentarios cuando hay una razón explícita para utilizar una biblioteca determinada.

En términos generales, lo más importante al momento de incorporar Comentarios al Código Fuente, es tener presente que deben ser útiles para otra persona. No son comentarios para (desde el punto de vista del desarrollador) y, al momento de escribirlos, es fundamental ser capaz de ponerse en el lugar del otro, el que, probablemente, mantendrá el Código Fuente.

Como indican Kernighan & Plauger en el libro The Elements of Programming Style:

"No documentes código malo - reescríbelo"

Y, como dice Steve McConnell, en su libro Code Complete:

"Buenos comentarios no repiten el código ni lo explican. Clarifican su intención. Los comentarios debieran explicar, en un nivel de abstracción más alto que el código, lo que se está haciendo".

Incorporar comentarios en el Código Fuente es un proceso importante y, en muchos casos, el proceso de reflexión requerido para determinar qué y cómo incluir un comentario puede ser una ayuda, incluso, para determinar si el código está bueno o malo.