Cuando estudias aplicaciones web seguro te has encontrado con el término "sesión". Una sesión es un mecanismo que permite al servidor "recordar" al usuario entre peticiones. Esto es crucial para la autorización: si no, tendrías que introducir usuario y contraseña en cada clic.
¿Cómo funciona una sesión? Cuando el usuario se autentica, el servidor crea un identificador único (Session ID). Ese identificador se envía al cliente (por ejemplo, el navegador) normalmente en forma de cookie. En cada petición posterior el cliente adjunta ese identificador, lo que permite al servidor saber quién es.
Gestión de sesiones en Spring Security
Spring Security incluye una funcionalidad potente para trabajar con sesiones. Veamos cómo configurarlas y gestionarlas.
Configuración de la política de sesiones
Una de las primeras cosas con las que te encontrarás es configurar la política de gestión de sesiones. Por ejemplo, puedes definir cuántas sesiones activas puede tener un usuario. Para eso puedes usar los métodos sessionManagement() y maximumSessions().
Ejemplo:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.maximumSessions(1) // Sólo una sesión por usuario
.maxSessionsPreventsLogin(true); // Evita el inicio de sesión al superar el límite
}
}
Aquí limitamos al usuario a una sesión activa. Si intenta entrar desde otro dispositivo, se le denegará el acceso.
Gestión del cierre de sesión
Spring Security permite configurar la lógica que se ejecuta cuando una sesión termina (por ejemplo, si la sesión expira o se cierra manualmente). Para eso puedes usar la interfaz SessionRegistry.
Un handler para el cierre de sesión podría verse así:
@Component
public class CustomLogoutHandler implements LogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
System.out.println("Sesión terminada: " + authentication.getName());
}
}
Almacenamiento de sesiones
Las sesiones pueden almacenarse en la memoria del servidor o en una base de datos. El uso de la base de datos es común en sistemas distribuidos, donde el balanceador de carga puede dirigir al usuario a diferentes servidores entre peticiones. En esos casos las sesiones deben ser "compartidas".
Tokens de seguridad: ¿por qué las sesiones no son suficientes?
En la era de los microservicios y las REST API, la idea de los tokens cobra protagonismo. Los tokens son cadenas compactas que representan cierta información (por ejemplo, el identificador de usuario). A diferencia de las sesiones, los tokens no requieren almacenamiento en el servidor.
Ventajas de los tokens
- Independencia del estado del servidor: los tokens se almacenan en el cliente, lo que facilita escalar la aplicación.
- Menor carga: el servidor no tiene que mantener estado por cada usuario.
- Comodidad para APIs: los tokens se pueden enviar en la cabecera de la petición HTTP, lo cual es ideal para REST API.
JWT (JSON Web Token)
JWT es un formato popular de tokens que consiste en una cadena JSON codificada. Cada token tiene tres partes:
- Header (cabecera): tipo de token y algoritmo de cifrado usado.
- Payload (carga útil): datos del token, por ejemplo
userIdorole. - Signature (firma): protege el token contra falsificación.
Ejemplo de token JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQiLCJyb2xlIjoiVVNFUiJ9.abc123signature
Uso de JWT en Spring Security
Vamos a añadir JWT a la seguridad de nuestra aplicación.
Para generar tokens necesitarás una librería, por ejemplo, io.jsonwebtoken (JWT). Aquí tienes un ejemplo de creación de token:
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtTokenUtil {
private static final String SECRET_KEY = "mySecretKey";
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 1 día
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
}
Validación de tokens
Antes de usar un token es importante verificar que es auténtico y que no ha expirado. Para eso hay que decodificarlo y comprobar la firma:
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
public class JwtTokenUtil {
private static final String SECRET_KEY = "mySecretKey";
public Claims validateToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
El método validateToken() devuelve el payload del token si es válido.
Configuración de la autenticación JWT
Ahora integremos JWT en Spring Security. Para ello creamos un filtro:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // Quitamos "Bearer "
Claims claims = jwtTokenUtil.validateToken(token);
if (claims != null) {
String username = claims.getSubject();
// Creamos el objeto de autenticación
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
El filtro verifica el token antes de que la petición continúe.
Ejemplo de uso de tokens en nuestra aplicación
Hemos creado un REST API para autenticar. En el controlador /login se devuelve un token JWT. El cliente debe enviar ese token en la cabecera Authorization en peticiones posteriores.
Controlador:
@RestController
public class AuthController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String username, @RequestParam String password) {
// Aquí normalmente compruebas username/password
String token = jwtTokenUtil.generateToken(username);
return ResponseEntity.ok(new HashMap<String, String>() {{
put("token", token);
}});
}
}
Ahora tu cliente puede hacer una petición autorizada:
curl -H "Authorization: Bearer <your_token>" http://localhost:8080/protected/resource
¿Cuándo usar sesiones y cuándo tokens?
Si trabajas con aplicaciones web tradicionales, las sesiones siguen siendo una gran opción. Son simples, cómodas y están bien integradas en el ecosistema Spring.
Sin embargo, para REST API y microservicios las sesiones no son tan cómodas. Aquí destacan los tokens, especialmente los JWT, que ofrecen movilidad, facilidad de escalado e independencia del estado del servidor.
GO TO FULL VERSION