Archivo

Archive for the ‘Desarrollo’ Category

Tiempo y zonas horarias en aplicaciones

Domingo, 1 junio 2014, 1:39 1 comentario

Imagen

Siempre me he encontrado con la duda de qué estándar de tiempo utilizar en mis aplicaciones o si debo hacer uso de manera explícita del uso horario o no. Así que he decidido analizar el asunto.

Empecemos distinguiendo los estándar de tiempo de las zonas horarias:

  • Los estándar de tiempos son los convenios establecidos para la medición de duración de tiempos o el establecimiento puntual de eventos en el tiempo. Es muy importante saber para qué vamos a utilizar o para qué necesitamos la medición de tiempos en cuestión. No es lo mismo que queramos definir momentos concretos en el tiempo, bien pasado o futuro; o medir duración de tiempo entre momentos específicos. Para cada caso nos convendrá un estándar u otro.
  • Los husos o zonas horarias o tiempos civiles son los tiempos definidos de manera legal en un territorio. Toman como referencia un estándar de tiempo (hoy en día UTC) y difieren de este un tiempo definido según el meridiano de la región. Pueden adoptar o no, cambios por horario de verano.

A continuación veamos los tipos de estándar de tiempo:

  • Estándares de tiempo basados en la rotación de la Tierra. Son aquellos en los que la posición astronómica de la Tierra y su medición indican cada momento puntual de tiempo o la duración del mismo.
    • GMT (Greenwich Mean Team). Fue el primero en establecerse. Se sincroniza con la hora medida en el Real Observatorio de Greenwich (a las afueras de Londres) que coincide con el meridiano 0 o de Greenwich. Se calcula mediante la duración media del día medida entre dos pasos del Sol por el meridiano. Fue sustituido por UTC desde 1972. Por cierto, las Islas Británicas solo coinciden con GMT durante el invierno.
    • UT (Universal Time). Definido con posterioridad a GMT, pero con su misma filosofía. Su medición, sin embargo, se basa en el posicionamiento diurno de las estrellas y no del Sol, lo que lo hace más exacto. Existen varios UT: UT0, UT1, UT1R y UT2. UT0 es la versión que más se ciñe a la definición de UT. UT1, es el más usado y en el que se basa UTC, incluye sobre UT0 las variaciones debidas al movimiento del eje de rotación de la Tierra. UT1R incluye las variaciones debidas a las mareas. UT2 es un suavizado periódico de UT1.
  • Estándares de tiempo construidos. Son aquellos que se sincronizan mediante eventos de duración definida:
    • TAI (International Atomic Time). Estándar de tiempo desde 1972 para la definición del segundo. Se creó para evitar las derivas de tiempos y las irregularidades propias de la rotación y traslación de la Tierra. El tiempo TAI se calcula mediante la sincronización de cerca de relojes atómicos en todo el mundo. Difiere de UTC en un número definido de segundos (los segundos intercalares añadidos o restados hasta la fecha desde su establecimiento).
    • UTC (Coordinated Universal Time). Es el estándar de tiempo más utilizado. Está basado en el TAI pero con una diferencia de segundos definida y conocida. Estos segundos son los segundos intercalares añadidos o restados (hasta la fecha siempre se han añadido) para evitar que la hora UTC no difiera de la hora UT1 de Greenwich más de 0,9 segundos.
    • GPST (GPS Time). Utilizado por el sistema de posicionamiento GPS. Está retrasado sobre TAI de manera constante 19 segundos. El mensaje de posicionamiento GPS incluye el número de segundos intercalares con UTC, para que las unidades GPS puedan calcular la hora UTC.
  • Estándares de tiempo basados en el movimiento de planetas. Son aquellos en los que la posición y el movimiento de los plantas indican cada momento puntual de tiempo o la duración del mismo. El más utilizado era el Tiempo de Efemérides, calculado a partir de la traslación de la Tierra alrededor del Sol, ya en desuso debido a que no tenía en cuenta las tesis relativistas del tiempo. Hoy en día existen otros que sí tienen en cuenta estos factores, como el Terrestrial Time que sustituyó al Tiempo de Efemérides.

Conclusiones

De manera general, podemos decir que el estándar de tiempo más apropiado a utilizar es UTC para el almacenamiento de información temporal. Formateando o capturando la información para adaptarla a la zona horaria del usuario en el momento de su procesado. Todos las zonas horarias están referenciadas a este estándar y la conversión hacia o desde estas es inmediata.

Sin embargo cuando queramos calcular duraciones de tiempo a futuro con precisión, no podremos utilizar UTC, ni cualquier otro sistema basado o sincronizado en cálculos astronómicos; puesto que no podemos conocer las variaciones de tiempo o segundos intercalares que se aplicarán a futuro a un estándar de tiempo. En ese caso es preferible utilizar TAI. Esta observación no aplica para el procesado de fechas: el evento en cuestión seguirá ocurriendo en dicho momento con independencia de las variaciones.

El almacenamiento y cálculo de tiempos y fechas con zonas horarias aplicadas no puede considerarse una buena práctica. En primer lugar, las zonas horarias con cambio de horario en verano no son causales (un evento posterior a otro puede aparecer como anterior si entre medias se produce el cambio de horario de otoño, el retraso de una hora), lo que dificulta el cálculo de duraciones de tiempo.En segundo lugar, dificulta la internacionalización de soluciones o la compartición de diferentes ámbitos regionales en una solución (aplicación usada en países con diferentes zonas horarias o usuarios que viajan entre zonas horarias); sobre todo si no se ha tenido en cuenta en el diseño inicial de la solución. El argumento de la mejora de rendimiento no suele ser válido para defender esta posibilidad tampoco, puesto que los cálculos para el cambio de zonas horarias de tiempos son inmediatos (por lo general, suma de un número entero de horas).

Hasta Windows 2000 los sistemas Windows almacenaban la hora local referenciada a la zona horaria establecida en el equipo o usuario. Desde Windows 2000 y en los sistemas UNIX-like, se almacena UTC (aunque se formatee a la zona horaria del usuario cuando se muestra).

Referencias

La foto está sacada de aquí.

Time Standard en Wikipedia 
GMT vs UTC

Licencia Creative Commons
Esta entrada de Juan Francisco Adame Lorite está publicada bajo una licencia Creative Commons Atribución-CompartirIgual 3.0 Unported.

Categorías:Desarrollo

Consejos y buenas prácticas del logging de aplicaciones

Martes, 2 octubre 2012, 23:52 2 comentarios

Hemos ido asumiendo con el tiempo que aspectos como la seguridad y las pruebas deben de tenerse en cuenta durante todo el ciclo de vida del proyecto, desde sus orígenes hasta su finalización. Que no son requisitos no funcionales deseables o secundarios, si no que son prioritarios como parte de la estructura fundamental del aplicativo (hace no tanto eran funcionalidades o mecanismos que se diseñaban e implementaban a la finalización del proyecto como un añadido, si se hacían). Sin embargo la traza de aplicación, aún habiendo hecho méritos, no ha recibido en muchos casos la misma consideración ;-P.

En esta entrada no creo que os descubra nada nuevo a los que tengáis experiencia en el desarrollo. La idea es hacer un ejercicio de síntesis y revisión para resumir lo que, en mi opinión, son las mejores o las buenas prácticas del log de aplicación. Por supuesto, las aportaciones, observaciones, matizaciones y críticas son bienvenidas; os invito a que os unáis a este ejercicio.

El log o traza de aplicación es el procesado y almacenamiento de información relativa a la ejecución de una aplicación. Contiene datos de entidades, cambios de estado y componentes software involucradas en dicha ejecución. Su principal funcionalidad es facilitar el seguimiento o análisis de la ejecución de la aplicación:

  • Analizar el comportamiento de la aplicación durante la fase de desarrollo y depuración (pruebas de caja blanca)
  • Analizar los bugs o errores de ejecución detectados, sus causas y consecuencias
  • Servir de registro de auditoría cuando la información contenida y el modo en que se ha procesado cumpla los criterios requeridos
  • Medir el rendimiento o carga de los sistemas o aplicaciones
  • Revertir el estado del aplicativo siguiendo en orden inverso el log

Aunque depende de las circunstancias en las que nos encontremos, el segundo de los usos suele ser el más relevante. Un buen log desarrollado correctamente en el código y mantenido o configurado en explotación es una garantía de respuesta rápida para análisis de errores; que incluso podrían hacerse sin necesidad de parar el aplicativo, reconfigurarlo o aplicarle ningún cambio.

La traza de aplicación suele formarse por un conjunto de eventos que se almacenan secuencialmente, por lo general en el orden en que suceden, de manera persistente o recuperable. Se pueden almacenar en ficheros, en BBDD, en componentes distribuidos a tal efecto… Se pueden habilitar mecanismos de rotación o historificación de estos logs, se pueden utilizar por monitores para lanzar alertas, se pueden integrar y fusionar para hacer análisis más exhaustivos… Lo relevante es que la información registrada y la forma en que se gestiona sea útil.

Existen numerosas soluciones y propuestas software, tanto libres como propietarias, más o menos estandarizadas, más sencillas o más completas, de mil tipos y formas. Lo importante es buscar aquella se ajusta a nuestras necesidades y entornos, para olvidarnos de la implementación del mecanismo por completo; y ceñirnos a los dos aspectos más importantes:

  • El contenido de cada registro o evento, principal preocupación del desarrollador
  • El modo en que se procesa, persiste y gestiona, principal preocupación de la explotación del sistema o aplicativo

El coste de implementación del logging se encuentra en el ir, mientras se desarrolla, dejando traza en los diferentes puntos del código. Esta actividad debe hacerse durante el desarrollo, siguiendo patrones, criterios y procedimientos preestablecidos. De esta manera los desarrolladores tendrán criterios comunes y la traza será coherente entre las diferentes partes del código.

La traza y la documentación del código pueden tener puntos en común en cuanto a su filosofía, objetivos y modo de aplicación, pero existe una diferencia importante entre ambos: mientras la documentación del código se adentra en el por qué se hace algo de esa manera, la traza debe cubrir el qué se hace. Por poner un ejemplo, en un bucle la documentación del código debería indicar por qué los límites o condiciones de salida son tales y la traza registrar durante la ejecución en qué punto se cumplió dicha condición de salida.

La información registrada en la traza debe ser relevante y completa. Se pueden considerar los siguientes aspectos a la hora de decidir qué información incluir:

  • Qué:
    • Qué evento o acción ha ocurrido.
    • Qué entidades han estado involucradas.
    • Si hay un cambio de estado, ¿cuál era el anterior? ¿cuál es el nuevo estado?.
  • Dónde:
    • En qué punto del código ha ocurrido: componente, clase, fichero de código, método o bloque de ejecución, línea de código… Cuanto más detallada sea esta información mejor para localizar el lugar del posible error o por donde ha pasado la ejecución, por un lado; pero más puede afectar el logging al rendimiento (se podría requerir información de debug) y a la concrección de la información de la traza, por otro.
  • Cuándo:
    • Registrando el momento temporal, bien absoluto o bien relativo al comienzo de ejecución o cualquier otro evento.
    • Generando una traza secuencial o causal, en la que los eventos que ocurren antes en el tiempo o que ocasionan otros, aparezcan antes.
  • En qué contexto:
    • Registrando estados o variables: propios de la ejecución (parámetros), de personalización o específicos de usuario, referentes a la sesión o transacción en ejecución…
    • Indicando hebras, transacciones o peticiones relacionadas cuando estemos en entornos concurrentes.

Para que la información de la traza sea más detallada en momento de análisis y más manejable durante la explotación de la misma, se establecen niveles de filtrado. De tal manera que solo se muestra o almacenan aquellos eventos con un nivel mayor o igual al del nivel de traza establecido. Las librerías de logging permiten filtrar los eventos por otros criterios como son la clase o contexto del evento, también.

Los niveles más comunes son DEBUG, INFO, WARNING y ERROR. La clasificación de los diferentes eventos en cada nivel es parte del ejercicio de análisis, y deben orientarse a que la traza sea legible y útil en los diferentes contextos del aplicativo, desde el desarrollo hasta la explotación.

Este puede ser un ejemplo de semántica de niveles de logging:

  • DEBUG, para información de muy bajo nivel solo útil para el debug de la aplicación, tanto en el desarrollo como en el análisis de incidencias
    • Llamadas a funciones y procedimientos y otros componentes, con parámetros y respuestas
    • Flujos de ejecución
    • Desarrollo de algoritmos y procedimientos que permitan identificar y seguir su ejecución en desarrollo
  • INFO, información de más alto nivel que permita hacer un seguimiento de la ejecución normal
    • Paradas y arranques de servicios y sistemas
    • Parámetros críticos o relevantes de configuración
    • Comienzo y fin de transacciones y operaciones completas
    • Cambios de estado de operaciones
  • WARN, información de situaciones, que aún sin ser de error, si son anómalas o no previstas, aunque el aplicativo tiene alternativas para solventarlas
    • Parámetros no definidos, y cuyo valor se toma por defecto
    • Situaciones anómalas, pero que son resueltas por el aplicativo, dejando la operación en un estado correcto
    • Funcionalidades no primordiales o imprescindibles, que no pueden resolverse, pero que dejan la operación en un estado correcto
  • ERROR, información de situaciones que son de error y que impiden la ejecución correcta de una operación o transacción, pero sin afectar a otras operaciones o transacciones (error aislado o contenido)
    • No se pudo realizar una operación o transacción, pero no afecta a otras
    • Peticiones o consultas erróneas (almacenando los parámetros de entrada)
    • Funcionalidades generales del aplicativo, que aún afectando al funcionamiento general del aplicativo, no se consideran primordiales o imprescindibles
  • FATAL, información de situaciones de error que afectan al funcionamiento general del aplicativo (errores no aislados o contenidos en alcance)
    • Parámetros no definidos o configuraciones erróneas
    • Falta de conexión o comunicación con otros componentes
    • Errores de ejecución que pueden afectar a operaciones o transacciones independientes, o que afectan al funcionamiento general de la aplicación

Tanto el contenido y forma de cada evento de log, como la semántica de los niveles son parte del diseño del aplicativo. Estos criterios deben definirse y ser comunicados al equipo de desarrollo para que se apliquen de manera homogénea y coherente en todo el desarrollo. Si estos criterios son consensuados con el equipo se puede sacar mucho provecho de la experiencia de los desarrolladores.

Otras recomendaciones a tener en cuenta son:

  • Registrar los eventos de manera atómica, que toda la información referente a un evento se almacene en un registro o línea.
  • Orientar el formato de la información mostrada a poder ser usado de manera informatizada o automática con herramientas específicas.
  • En lo referente a excepciones:
    • Mostrar siempre la traza completa de la excepción con su mensaje y todo el stacktrace.
    • Si la excepción es capturada, tratada y luego lanzada de nuevo (bien la misma u otra) no dejar traza de la excepción hasta que se capture y se trate en última instancia. De esta manera cada excepción solo se mostrará una vez. Si no lo hacemos así y registramos cada captura y relanzamiento aparacerá en la traza varias veces y no sabremos si es la misma o son varias. (Antipatrón catch-log-throw).
    • Hacer uso de la propiedad de excepción causante o interna cuando capturemos y relancemos la excepción. En Java es el método getCause(), en .NET la propiedad InnerException.
  • Hacer uso de mecanismos de MDC/NDC (información de contexto) para entornos de múltiples hebras o ámbitos de transacción. Esta información nos puede permitir filtrar o concretar la información de log  en contexto de negocio específicos como pueden ser clientes, usuarios, productos…
  • Implementar el método toString() de todas las clases de negocio o POJOs para facilitar su traceado.
  • Hay que tener en cuenta las diferencias y zonas horarias a la hora de trabajar con logs de varias máquinas o componentes.
  • El logging no puede tener efectos laterales, no puede modificar el estado de ningún parámetro, variable o procedimiento. Solo muestra información. Debe ser ligero, el propio logging no puede requerir de procesamientos largos o costosos.
  • Las llamadas a métodos, componentes y componentes distribuidos externos, deberían registrarse tras la llamada incluyendo los parámetros de entrada y la respuesta, así como clase/componente y método, a nivel de DEBUG. Y a nivel de ERROR y FATAL cuando ocurra un error en la llamada (si fuese una excepción se debe tracear parámetros de entrada y excepción)

Un saludo,

Juan Francisco Adame Lorite

Licencia Creative Commons

Categorías:Desarrollo Etiquetas:

Los pequeños detalles sí importan

Domingo, 23 septiembre 2012, 23:24 Deja un comentario

He descubierto hace poco un blog muy sencillo y minimalista referente a los interfaces de usuario y a ejemplos de detalles de calidad que se pueden encontrar en muchos sitios y software. Se llama Little Big Details.
Cada entrada hace referencia a un ejemplo de característica o funcionalidad de UI, a modo de pildorilla, que muestra una especial calidad o cuidado en su diseño y ejecución y que nos puede servir de ejemplo para cuando nos toque diseñar uno.
Una buena idea y muy práctica.

Categorías:Desarrollo, UI, Web Etiquetas:

Feedback y canales de comunicación en interfaces de usuario (UI)

Domingo, 8 abril 2012, 12:42 2 comentarios

Leía y veía esta entrada hace poco en Genbeta:dev y me pareció muy recomendable.


En relación con lo que hablábamos en la entrada anterior la exposición de Bret es muy buena.  Comienza ganándose al público con golpes de efecto: los prototipos que presenta son muy llamativos y representativos de lo que quiere mostrar, nos enseña un buen conjunto de ellos y están enfocados al auditorio para el que habla, que seguro que son desarrolladores en su mayoría.
Una vez tiene el público ganado, desarrolla toda su exposición de manera lineal, como si fuese una historia, explicando cuál es el motivo y motor de su trabajo hasta llegar al concepto del principio (principle) que, según él, mueve toda iniciativa creadora o emprendedora. Este es el punto álgido de la exposición en el que con un entusiasmo contagioso y una credibilidad envidiable cierra su exposición. Lo que trata tiene sustancia y cuerpo (no es humo) y además, en mi opinión, lo presenta muy bien. Si tuviera que sacar un pero, el tiempo, se me antoja un poco larga; pero posiblemente le pidieron intervenir durante una hora.

De lo que trata Bret es de los tiempos de respuesta o feedback de los interfaces de usuario. Se centra en los entornos de desarrollo, pero es extrapolable a otros ámbitos. Si aplicamos la Teoría de la Comunicación Humana, en toda comunicación existe un emisor (el usuario), un receptor (el sistema), un mensaje (qué quiere hacer el usuario), un canal y un código (ambos representados en el interfaz de usuario). El primer axioma de Paul Watzlawick dice “Es imposible no comunicar”, pero esto no es siempre es verdad cuando el receptor es una máquina (¿esto se ha quedado colgado?”, “¿cuánto falta para que acabe?”). El hilo conductor que mantiene toda conversación es la retroalimentación del receptor al emisor. Los interfaces de usuario llevan trabajando las últimas décadas en este sentido: el relojito de espera de los entornos Windows se creó para indicar al usuario que el equipo estaba realizando una tarea y que no estaba colgado, la barra de estado y de progreso se crearon a continuación para indicar el estado de realización de dicha tarea y hoy en día existen opciones que dan información más completa del estado de la máquina y sus tareas a su interlocutor, como cuadros de diálogo que aventuran una tiempo de finalización.

Volviendo al punto de vista del desarrollo es muy común estar resolviendo problemas en los que, o bien porque su solución es muy compleja o porque la plataforma utilizada no es lo suficientemente eficiente, el retorno que recibimos no llega a tiempo o no nos da suficiente información. Los tiempos de compilación son muy grandes, como para probar cada pequeño cambio; o el despliegue de la solución y el camino a seguir para replicar la situación que queremos comprobar es muy largo. Esto ocasiona que el desarrollador no tenga retorno por parte del sistema del trabajo que está haciendo y reduzca mucho su productividad, puesto que la vuelta atrás en caso de error puede venir muy tarde.

Existen soluciones como hacer un buen diseño en profundidad, pero el trabajo del desarrollador sigue siendo principalmente creativo y un diseño no puede cubrirlo todo. Otras alternativas más adecuadas y elegantes son el uso de pruebas unitarias de restrinjan el alcance y ámbito a probar al punto de desarrollo en que nos encontramos. Su ejecución es mucho más sencilla y rápida que el despliegue y ejecución de toda la solución y nos pueden estar dando feedback incluso en segundo plano mientras desarrollamos. Pero el desarrollo de estas pruebas unitarias, de los conjuntos de datos para lanzarlas y de los mockup necesarios para emular las situaciones a probar requieren un tiempo de desarrollo para nada despreciable y muchas veces análogo al de la solución en ciernes. Es en este punto donde se centra la solución propuesta por Bret, que sea la plataforma la que provea de mecanismos rápidos para la definición en el propio código del alcance de la prueba unitaria y para establecer la condiciones de entorno de la prueba (parámetros de entrada, estado del aplicativo y respuesta esperada). Una idea que mejoraría muchísimo la productividad de los entornos de desarrollo.

Un saludo,

Licencia Creative Commons
Esta entrada de Juan Francisco Adame Lorite está bajo una licencia Creative Commons Atribución-CompartirIgual 3.0 Unported.

Categorías:Desarrollo Etiquetas:

Login en GWT con OpenID y Spring Security

Sábado, 4 febrero 2012, 3:00 9 comentarios

En la última entrada hablábamos de delegar la autenticación a terceros cuando la gestión de esas credenciales no aportase valor o fuese un requisito evitable. Entre las opciones que existen, una de ellas es el estándar OpenID, ya en su versión 2.0.

El objetivo de esta entrada es crear un aplicativo web que autentique la sesión en el servidor a través de OpenID mediante Google Accounts (Cuentas de Google). Todo ello lo integraremos dentro de la arquitectura de Spring Security. En este caso es un aplicativo web basado en la plataforma GWT; pero el 99% del peso funcional recae sobre Spring, por lo que es muy fácilmente portable a aplicativos web basados en otras tecnologías. Para el desarrollo de las llamadas a OpenID hemos utilizado OpenId4Java.

OpenId

OpenID es un estándar de autenticación descentralizado, que permite a una entidad (Proveedor OpendID) identificar usuarios para una tercera (Tercero de confianza) con la que establece esa asociación de confianza. La gestión de credenciales y los mecanismo de autenticación, quedan por completo bajo la responsabilidad de la primera entidad. El Tercero de confianza acepta las identificaciones que el Proveedor OpenID le ofrece; pero es el responsable de la autorización de acceso a los recursos propios. De esta manera, credenciales y autenticación quedan completamente separados e independientes de recursos y autorización. OpenID no describe el modo en que se realizan cualquiera de estas dos funciones, solo cubre el modo en que ambas entidades se comunican.

En OpenID interactúan entre sí:

  • Agente de usuario, que es el navegador que utiliza el usaurio
  • Tercero de confianza, que es el aplicativo o recurso al que el usuario desea acceder con las credenciales del Proveedor OpenID
  • Proveedor OpenID, que autentica al usuario y envía las credenciales validadas al Tercero de confianza para autorizar el acceso al recurso

La secuencia de interacción entre las diferentes partes es la siguiente:

  1. El usuario desea acceder a un recurso del Tercero de confianza. El tercero tiene que autorizar al usuario, por lo que debe identificarle primero. De entre las opciones para identificarle que se le ofrecen al usuario está a través de OpenID con un Proveedor.
  2. El usuario elige la opción de identificarse mediante OpenID
  3. El Tercero de confianza comienza la búsqueda o descubrimiento de los diferentes Proveedores OpenID
  4. El Tercero de confianza elige uno de los Proveedores y crea una asociación con él. Esta asociación contiene un secreto compartido que protege las sucesivas comunicaciones
  5. El Tercero de confianza, redirige al Agente de usuario al Proveedor OpenID. El Proveedor autentica al usuario y autoriza el acceso a atributos del usuario en el Proveedor si han sido solicitados
  6. El Proveedor OpenID redirige al Agente de usuario a una dirección de retorno preestablecida en el Tercero de confianza, junto con las credenciales del usuario
  7. El Tercero de confianza utiliza estas credenciales para autorizar el acceso al usuario

La solución

El usuario accede al aplicativo GWT y Spring Security, al no estar autenticado, le redirige a la pantalla de login. Entre las opciones de autenticación se le permite al usuario hacerlo mediante Google Accounts (Cuentas de Google). El usuario pulsa el botón o enlace en cuestión y es redirigido a Google para autenticarse.

Debemos primero redirigir al usuario hacia la web del Proveedor OpenID de Google. Para ello debemos crear la URL completa con todos los parámetros que nos redirija. Como queremos asociar la sesión del servidor a estas credenciales, haremos toda esta tarea en el lado del servidor mediante un método RPC de GWT que nos descubra el Proveedor OpenID, nos realice la asociación y nos genere la URL. Este método RPC nos devolverá la URL con los parámetros para redirigir al usuario al servidor de autenticación OpenID.

@UiHandler("googleAccountsButton")
void onGoogleAccountsButtonClick(ClickEvent event) {
	// Invocamos al servicio que nos genera la query string al servidor OpendID
	final LoginServiceAsync loginService = GWT.create(LoginService.class);
	loginService.requestOpenIdLogin(new AsyncCallback() {

		public void onFailure(Throwable error) {
			Window.alert("Error: "+error.getMessage());
		}

		public void onSuccess(String openIDQueryString) {
			// Redirigimos al servidor de autenticación OpenID
			Window.open(openIDQueryString, "_self", "");
		}
	});
}

En el lado del servidor, en la implementación del servicio que invocábamos antes:

  1. Descubrimos los servidores OpenID
  2. Creamos una asociación (que almacenamos en la sesión de usuario), para vincular la sesión del usuario actual con las credenciales que el servidor OpenID nos devuelva
  3. Generamos la URL para redirigir al usuario al servidor OpenID. Esta solicitud contiene además los atributos del usuario que queremos que el Proveedor OpenID nos devuelva. Los atributos del usuario no son más que datos personales asociados a la identidad del Proveedor OpenID. Se recupera mediante una extensión de OpenID, OpenID Attribute Exchange
package com.juanfran.server;

import java.util.List;

import org.openid4java.consumer.ConsumerManager;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.ax.FetchRequest;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.juanfran.client.LoginService;

@SuppressWarnings("serial")
public class LoginServiceImpl extends RemoteServiceServlet implements
		LoginService {

	public String requestOpenIdLogin() throws Exception {
		ConsumerManager manager = new ConsumerManager();

		// Obtenemos la información del servidor de autenticación de OpenID
		@SuppressWarnings("unchecked")
		List discoveries = manager
				.discover("https://www.google.com/accounts/o8/id");

		// Creamos asociación con el servidor y la almacenamos en la sesión del
		// usuario
		DiscoveryInformation discovered = manager.associate(discoveries);
		this.getServletContext().setAttribute("discovered", discovered);

		// Creamos solicitud de atributos (OpenID Attribute Exchange)
		FetchRequest fetch = FetchRequest.createFetchRequest();
		fetch.addAttribute("FirstName", "http://axschema.org/namePerson/first",
				true);
		fetch.addAttribute("LastName", "http://axschema.org/namePerson/last",
				true);
		fetch.addAttribute("Email", "http://axschema.org/contact/email", true);
		fetch.addAttribute("Country",
				"http://axschema.org/contact/country/home", true);
		fetch.addAttribute("Language", "http://axschema.org/pref/language",
				true);
		fetch.addAttribute("data", "http://example.com/schema/data", true);

		// Creamos la petición de autenticación y le añadimos
		AuthRequest authRequest = manager.authenticate(discovered, String
				.format("http://%s:%d/Login/authResponse", this
						.getThreadLocalRequest().getServerName(), this
						.getThreadLocalRequest().getServerPort()));
		authRequest.addExtension(fetch);

		// Devolver la query string para redirigir al servidor de autenticación
		// de OpenID
		return authRequest.getDestinationUrl(true);
	}
}

El código onSuccess() de GWT nos redirige a la URL generada en el servidor. Navegamos al Proveedor OpenID, que en este caso es Google. El mecanismo por el que el servidor OpenID nos autentica está fuera del alcance de OpenID, puede ser cualquiera. Nuestro aplicativo confía en el servidor, en las credenciales que remita y en que el mecanismo de autenticación utilizado es seguro.
El Proveedor OpenID, una vez nos hemos autenticado, nos indica si queremos que el acceso al dominio del Tercero de confianza, 127.0.0.1 (porque está corriendo local en mi máquina), sea autorizado con estas credenciales (Cuenta de Google en este caso). Así mismo también nos informa de que, además de las credenciales (que es un token del que no se puede deducir ninguna información adicional), se solicita acceso a algunos atributos personales de las credenciales como son mi nombre completo, el mail y el idioma. No podemos autorizar unas cosas sí, y otras no; autorizamos o denegamos todo en su conjunto.

Solicitud de permisos en Cuentas de Google

Solicitud de permisos en Cuentas de Google

Si autorizamos el acceso, el navegador nos redirigirá de nuevo a la URL de retorno que pasamos como parámetro en la URL, “/Login/authResponse”. En esta URL dentro de los filtros de Security Spring se encuentra escuchando nuestra implementación de AbstractAuthenticationProcessingFilter. Este filtro:

  1. Recoge los parámetros enviados por el servidor OpenID
  2. Verifica que la autenticación haya sido correcta
  3. Extrae los atributos personales del usuario
  4. Crea el objeto Authentication que contiene las credenciales y las devuelve para que Security Spring las gestione y asocie a la sesión del usuario

El filtro debe devolver una clase Authentication o una excepción si no ha sido posible validar la autenticación.

package com.juanfran.server;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.openid4java.association.AssociationException;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.consumer.VerificationResult;
import org.openid4java.discovery.DiscoveryException;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.discovery.Identifier;
import org.openid4java.message.AuthSuccess;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.openid4java.message.ax.AxMessage;
import org.openid4java.message.ax.FetchResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

/**
 * Clase que implementa el filtro de autenticación HTTP para capturar las
 * respuestas con las credenciales del servidor OpenID.
 *
 * @author juanfran
 *
 */
public class OpenIdAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {

	/**
	 * Constructor del filtro de autenticación
	 *
	 * @param url
	 *            URL a la que se espera que lleguen las peticiones con las
	 *            credenciales para autenticar. En nuestro caso, es la URL de
	 *            retorno que se le indica al OpenID Authentication server para
	 *            que devuelva las credenciales una vez autenticado.
	 */
	public OpenIdAuthenticationFilter(String url) {
		super(url);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException,
			IOException, ServletException {
		// Creamos un tipo ParameterList
		ParameterList openIDParams = new ParameterList(
				request.getParameterMap());

		// Recuperamos la asociación de la sesión del usuario
		DiscoveryInformation discovered = (DiscoveryInformation) this
				.getServletContext().getAttribute("discovered");

		// Creamos la URL con parámetros incluidos
		StringBuffer receivingURL = request.getRequestURL();
		String queryString = request.getQueryString();
		if (queryString != null && queryString.length() > 0)
			receivingURL.append("?").append(request.getQueryString());

		try {
			ConsumerManager manager = new ConsumerManager();

			// Verificamos la
			VerificationResult verification = manager.verify(
					receivingURL.toString(), openIDParams, discovered);

			// Obtenemos las credenciales autenticadas
			Identifier verified = verification.getVerifiedId();

			// Si hay credenciales es porque se ha autenticado
			if (verified != null) {
				// Obtenemos los atributos de la cuenta
				AuthSuccess authSuccess = (AuthSuccess) verification
						.getAuthResponse();
				if (authSuccess.hasExtension(AxMessage.OPENID_NS_AX)) {
					FetchResponse fetchResp = (FetchResponse) authSuccess
							.getExtension(AxMessage.OPENID_NS_AX);

					String name = fetchResp.getAttributeValue("FirstName")
					 + " " + fetchResp.getAttributeValue("LastName");
					String mail = fetchResp.getAttributeValue("Email");

					// Almacenamos el mail en sesión por si queremos utilizarlo posteriormente
					request.getSession().setAttribute("mail", mail);

					// Creamos la clase Authentication que almacena las credenciales
					Authentication verifiedAuth = new OpenIdAuthentication(
							mail, verified.getIdentifier());
					verifiedAuth.setAuthenticated(true);
					return verifiedAuth;
				} else {
					// Si no se recogieron atributos
					throw new AuthenticationServiceException(
							"Attributes not retrieved");
				}
				// Si no las hay
			} else {
				throw new BadCredentialsException("Not authenticated");
			}

		} catch (MessageException e) {
			e.printStackTrace();
			throw new AuthenticationServiceException(e.getMessage(), e);
		} catch (DiscoveryException e) {
			e.printStackTrace();
			throw new AuthenticationServiceException(e.getMessage(), e);
		} catch (AssociationException e) {
			e.printStackTrace();
			throw new AuthenticationServiceException(e.getMessage(), e);
		}
	}

}

La clase Authentication creada es una instancia de OpenIdAuthentication. Esta clase implementa Authentication. También implementa Principal, pero es solo para poder devolverse a si misma en el método getPrincipal() y ahorrarnos la implementación de una clase. Por defecto, a todos los usuarios se les asigna el rol ROLE_USER.

package com.juanfran.server;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

/**
 * Clase que contiene las credenciales
 *
 */
@SuppressWarnings("serial")
public class OpenIdAuthentication implements Authentication, Principal {

	private boolean authenticated = false;
	Collection authorities;
	private String name;
	private String openId;

	/**
	 * Constructor.
	 *
	 * @param name
	 *            nombre
	 * @param openId
	 *            URI que identifica la usuario
	 */
	public OpenIdAuthentication(String name, String openId) {
		this.name = name;
		this.openId = openId;
		authorities = new ArrayList();
		//Por defecto se le asigna el role "ROLE_USER"
		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
	}

	/**
	 * Nombre (de Principal)
	 */
	public String getName() {
		return name;
	}

	/**
	 * Authority o permiso otorgado a estas credenciales
	 */
	public Collection getAuthorities() {
		return authorities;

	}

	/**
	 * Credenciales
	 */
	public Object getCredentials() {
		return openId;
	}

	/**
	 * Detalles de usuario. No implementado
	 */
	public Object getDetails() {
		return null;
	}

	/**
	 * Identidad identificada con estas credenciales
	 */
	public Object getPrincipal() {
		return this;
	}

	/**
	 * Indica cuando ha sido autenticado y verificado
	 */
	public boolean isAuthenticated() {
		return this.authenticated;
	}

	/**
	 * Indica cuando ha sido autenticado y verificado
	 */
	public void setAuthenticated(boolean arg0) throws IllegalArgumentException {
		this.authenticated = arg0;

	}
}

Ahora debemos configurar Spring Security y el Servlet de la llamada RPC en el web.xml.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

	<!-- Spring Security -->
	<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>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Servlets -->
	<servlet>
		<servlet-name>loginServlet</servlet-name>
		<servlet-class>com.juanfran.server.LoginServiceImpl</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>loginServlet</servlet-name>
		<url-pattern>/Login/login</url-pattern>
	</servlet-mapping>

	<!-- Default page to serve -->
	<welcome-file-list>
		<welcome-file>Hello.jsp</welcome-file>
	</welcome-file-list>

</web-app>

Por último, creamos todas las Beans y contexto de Spring en el fichero applicationContext.xml.
Como nuestro filtro OpenIdAuthenticationFilter sustituye al tradicional filtro web de usuario y contraseña UsernamePasswordAuthenticationFilter, no podemos utilizar auto-config="true". Esto nos obliga a definir el entry-point. Es una particularidad de configuración de Spring.
El filtro OpenIdAuthenticationFilter tiene referencias a los Handler correspondientes en caso de autenticación correcta y errónea, junto con sus URL de cada caso y su referencia al AuthenticationManager.
Para separar la parte pública de la privada, la aplicación GWT se ha dividido en dos módulos diferenciados, Login y Hello. Login es el encargado de las tareas de autenticación, por eso es accesible anónimamente; y Hello es la parte protegida, que solo es accesible una vez autenticado y autorizado.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">

	<http auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
		<intercept-url pattern="/Login.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<intercept-url pattern="/Login/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<intercept-url pattern="/Hello/**" access="ROLE_USER" />
		<intercept-url pattern="/Hello.jsp" access="ROLE_USER" />
		<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<logout logout-success-url='/Login.html?gwt.codesvr=127.0.0.1:9997' />
		<custom-filter position="FORM_LOGIN_FILTER" ref="authenticationFilter" />
	</http>

	<beans:bean id="loginUrlAuthenticationEntryPoint"
		class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<beans:property name="loginFormUrl"
			value="/Login.html?gwt.codesvr=127.0.0.1:9997" />
	</beans:bean>

	<beans:bean id="authenticationFilter"
		class="com.juanfran.server.OpenIdAuthenticationFilter">
		<beans:constructor-arg type="String"
			value="/Login/authResponse" />
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="authenticationFailureHandler"
			ref="failureHandler" />
		<beans:property name="authenticationSuccessHandler"
			ref="successHandler" />
	</beans:bean>

	<beans:bean id="successHandler"
		class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
		<beans:property name="defaultTargetUrl"
			value="/Hello.jsp?gwt.codesvr=127.0.0.1:9997" />
	</beans:bean>
	<beans:bean id="failureHandler"
		class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
		<beans:property name="defaultFailureUrl"
			value="/Login.html?gwt.codesvr=127.0.0.1:9997" />
	</beans:bean>

	<authentication-manager alias="authenticationManager" />
</beans:beans>

El host y el puerto en que está desplegado el aplicativo se deben a que está configurado para ser ejecutado localmente por el servidor Jetty de GWT. Y el parámetro “gwt.codesvr=127.0.0.1:9997” en todas las URLs se incluye para permitir el debug.

Un saludo.

Licencia Creative Commons
Esta entrada de Juan Francisco Adame Lorite está bajo una licencia Creative Commons Atribución-CompartirIgual 3.0 Unported.

Categorías:Desarrollo, Seguridad, Web Etiquetas: , ,
A %d blogueros les gusta esto: