Datos de sesión externalizados con Spring Session

Escrito por el , actualizado el .
java planeta-codigo programacion spring
Enlace permanente Comentarios

Java

Por defecto los datos de la sesión de una aplicación web Java se guardan en el servidor de aplicaciones y en memoria, esto produce que al reiniciar el servidor por un despliegue los datos de la sesión se pierdan y provoque en los usuarios alguna molestia como tener que volver a iniciar sesión. En Tomcat existe la posibilidad de que los datos de las sesiones sean persistidas en disco con la opción saveOnRestart del elemento de configuración Manager que evita que los datos de las sesiones se pierdan en los reinicios, al menos para los servicios formados por una única instancia. Para evitar que los usuarios perciban los reinicios o caídas del servidor hay varias soluciones algunas tratando de diferentes formas externalizar las sesiones del servidor de aplicaciones. Con estas soluciones se pueden hacer despliegues sin caídas, sin que las perciban los usuarios, siendo útil para hacer actualizaciones frecuentemente, continuos, y en cualquier momento cuando tengamos una nueva versión de la aplicación.

Las soluciones más comentadas son:

  • Cluster de servidores: para evitar las caídas podemos formar un cluster de máquinas de forma que si una se reinicia las peticiones sean atendidas por el resto de servidores del cluster. Añadiendo una poca configuración se puede formar un cluster de servidores Tomcat. Si el cluster está formado por unos pocos servidores esta solución es válida pero si el cluster es grande (¿media docena de máquinas?) el tráfico que se genera para sincronizar los datos de sesión en todas las máquinas puede ser significativo, momento en el cual se opta por otras soluciones.
  • Sesión en base de datos relacional: los datos de la sesión se pueden guardar en una base de datos relacional, al llegar una petición al servidor se recupera de la base de datos la sesión con una consulta y al finalizar la petición se lanza otra consulta de actualización. En las aplicaciones la base de datos suele ser un cuello de botella prefiriéndose guardar la sesión en otro servidor que no sea el servidor de base de datos para no generarle más carga.
  • Caché externa: en esta opción los datos se guardan en un servidor externo al servidor de aplicaciones de forma que todos los servidores del cluster las compartan pero no en la base de datos relacional, algunas opciones que se pueden utilizar son memcached o Redis que almacenan los datos en memoria y son muy rápidas. Esta opción añade una pieza más a la infraestructura de la aplicación que hay que mantener. En este artículo pondré un ejemplo usando esta opción utilizando Spring Session y un servidor Redis.
  • Sesión en cookie: para no añadir una pieza más a la infraestructura del servidor se puede externalizar la sesión en el cliente mediante una cookie. Como la cookie es enviada por el navegador cliente en cada petición el servidor puede recuperar los datos de la sesión. Sin embargo, como los datos son guardados en el cliente los datos de la cookie han de ser cifrados y firmados digitalmente para evitar problemas de seguridad ante modificaciones de los datos. También deberemos evitar guardar muchos datos y tendremos cierta limitación para que la cookie no sea grande, el tamaño recomendado no exceder es 4096 bytes si lo hacemos puede que ocasionemos errores con el mensaje 400 bad request, request header or cookie too large y consuma mucho ancho de banda, hay que tener en cuenta que las cookies son enviadas en cada petición al servidor origen no solo para las peticiones dinámicas sino también para los recursos estáticos como imágenes u hojas de estilos, si las cookies son grandes y el número de usuarios también el ancho de banda consumido por las cookies puede ser significativo, en estos últimos casos empleando un CDN puede aliviarse el tráfico generado. En la siguiente página están recogidos los límites de las cookies para cada navegador y el número máximo por dominio.

Usando Spring Session se puede externalizar los datos de la sesión en un servidor Redis usándolo como caché externa. Para demostrar y enseñar el código necesario he creado una pequeña aplicación web con Spring Boot. El controlador no tiene nada especial, obtiene la sesión y guarda los datos enviados en un formulario en la sesión, luego esta transparentemente se serializa en Redis. Usando la anotación @SpringBootApplication con la autoconfiguración se activa la infraestructura necesaria en el contenedor de Spring para guardar los datos de la sesión en Redis incluida la conexión a Redis. Por supuesto hay que añadir las dependencias necesarias al proyecto entre ellas el cliente Java de Redis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package io.github.picodotdev.springsession;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;

@SpringBootApplication
@ComponentScan("io.github.picodotdev.springsession")
public class Main {

	public static void main(String[] args) throws Exception {
		SpringApplication.run(Main.class, args);
	}
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.picodotdev.springsession;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class SessionController {

	@RequestMapping(value = "/")
	public String index() {
		return "index";
	}
	
	@RequestMapping(value = "/attributes", method = RequestMethod.POST)
	public String post(@RequestParam(value = "attributeName", required = true) String name, @RequestParam(value = "attributeValue", required = true) String value, HttpSession session, Model model) {
		session.setAttribute(name, value);
		return "redirect:/";
	}
}
SessionController.java
1
2
3
4
redis:
    image: redis:alpine
    ports:
        - 6379:6379
docker-compose.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apply plugin: 'java'
apply plugin: 'application'

mainClassName = 'io.github.picodotdev.springsession.Main'

ext {
    versions = [
        jedis: '2.9.3',
        springBoot: '2.1.3.RELEASE',
    ]
}

repositories {
    mavenCentral()
}

dependencies {
    implementation platform("org.springframework.boot:spring-boot-dependencies:$versions.springBoot")

    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-autoconfigure")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")    
	implementation("org.springframework.session:spring-session-data-redis")
    implementation("redis.clients:jedis:$versions.jedis")

	compileOnly("javax.servlet:javax.servlet-api:3.1.0")
}
build.gradle

Descargado el código fuente de la aplicación de ejemplo y utilizando Docker para iniciar el servidor Redis se puede iniciar la aplicación con el comando:

1
2
$ cd misc/docker/springsession
$ docker-compose up
docker-compose.sh
1
2
$ ./gradlew run

gradlew.sh

Lanzando una petición se puede ver como el Redis se guardan los datos de la sesión. Deteniendo la aplicación e iniciándolo de nuevo los datos de la sesión no se pierden al estar persistidos en Redis, el navegador envía la cookie de sesión que contiene únicamente su identificativo y la aplicación recupera los datos de Redis.

Dato en sesión Cookie de sesión en el navegador

Examinando los datos en Redis se puede ver que se ha creado una clave con el mismo identificativo de la cookie de sesión, en la clave están guardados los valores serializados entre ellos el nombre del atributo y su valor y otros datos como la fecha de creación, el último acceso y el intervalo máximo de inactividad antes de la expiración.

Contenido sesión en redis

En el momento de escribir este artículo Spring Session es un proyecto reciente y solo soporta la opción de Redis como caché externa pero seguramente con nuevas versiones soporte otras opciones como memcached, guardar la sesión en una cookie o en una base de datos relacional. La solución propuesta por Spring Session es válida para cualquier servidor de aplicaciones ya que se basa en crear un filtro en la aplicación que proporciona una versión modificada de HttpSession mediante el cual se guardan los datos de forma externa.

Otras posibilidades ofrecidas por Spring Session son múltiples sesiones en la misma instancia del navegador y soporte para aplicaciones REST y WebSocket. Para aumentar la seguridad se puede aumentar el tamaño del identificativo de la sesión almacenada en su cookie.


Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  9. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  10. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  11. Balanceo de carga y resiliencia en un microservicio con Spring Cloud Netflix y Ribbon
  12. Proxy para microservicios con Spring Cloud Netflix y Zuul
  13. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  14. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  15. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  16. Trazabilidad en microservicios con Spring Cloud Sleuth
  17. Implementar tolerancia a fallos con Resilience4j
  18. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  19. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring
  20. Microservicios con Spring Cloud, Consul, Nomad y Traefik
  21. Trazabilidad en servicios distribuidos con Sleuth y Zipkin
  22. Configuración de una aplicación con Spring Boot y configuración centralizada con Spring Cloud Config
Comparte el artículo: