¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
dev
Yavé Guadaño Ibáñez Hace 7 días Cargando comentarios…
Continuamos con nuestra serie sobre patrones de arquitectura de microservicios, y en esta ocasión hablaremos de seguridad. Si te has perdido alguno de los posts anteriores de esta serie, te animo a que les eches un vistazo 😉:
En un entorno de microservicios, la funcionalidad de una aplicación grande se fragmenta en múltiples servicios independientes, cada uno con responsabilidades concretas.
Seguimos con el hilo conductor de todos los posts que venimos haciendo, el ejemplo de un e-commerce, donde podría ser:
Cada uno de estos servicios se comunicará con el resto (o con un gateway) para completar los flujos de negocio. Sin embargo, cuando se construye un sistema distribuido, surge el desafío de cómo asegurar que cada solicitud proviene de un cliente legítimo y que las operaciones que se desean realizar están debidamente autorizadas.
Tradicionalmente, cuando se construía un sistema monolítico, la seguridad se gestionaba habitualmente mediante:
En microservicios, esto se complica porque:
En cualquier aplicación la seguridad es crucial si (poniendo de ejemplo un ecommerce):
Por lo tanto, asegurar que las comunicaciones están correctamente autenticadas y que cada usuario solo acceda a sus recursos es fundamental.
Para este artículo, nos centraremos en tres patrones que suelen usarse en la práctica.
Si bien existen otros enfoques (SAML, Basic Auth sobre TLS, Kerberos, etc.), estos tres se han vuelto especialmente populares en el contexto de arquitecturas modernas de microservicios y APIs REST.
Nota. Como veremos, vamos a dividir en 2 las entregas de estos patrones. En el primero veremos Token-based Authentication y Oauth, mientras que en el segundo, JWT y alguna comparativa más interesante.
Nota 2. Los ejemplos de código son únicamente para entender los conceptos. Puede haber partes incompletas o con trozos demostrativos que no aplicarían a entornos reales.
En la autenticación basada en tokens, un cliente (por ejemplo, una aplicación web en JavaScript, una app móvil, etc.) envía credenciales (usuario, contraseña) a un servicio de autenticación. Tras validar las credenciales, dicho servicio emite un token (comúnmente un string único, por ejemplo un UUID o un hash). Este token se utiliza en adelante para todas las peticiones posteriores: el cliente lo adjunta normalmente en la cabecera Authorization (generalmente Bearer <token>
Ventajas:
Desventajas:
Supongamos que tenemos:
El flujo sería algo así:
El diagrama de flujo quedaría así:
El diagrama de secuencia:
Veamos un ejemplo de cómo se puede configurar esta autenticación en un proyecto Java con Spring Boot.
En tu pom.xml (asumiendo Maven), podrías tener:
<dependencies>
<!-- Para seguridad básica en Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Para gestionar datos en Redis, si usamos Redis para tokens -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Otras dependencias necesarias para tu e-commerce -->
<!-- ... -->
</dependencies>
Si optamos por almacenar tokens en Redis, configuramos en application.yml:
spring:
redis:
host: localhost
port: 6379
Este servicio centraliza la lógica de:
@Service
public class TokenService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// Duración de los tokens en segundos, por ejemplo 2 horas
private final long TOKEN_EXPIRATION_SEC = 7200;
public String generateTokenForUser(String username) {
String token = UUID.randomUUID().toString();
// Guardamos en Redis con expiración
// Podríamos guardar un objeto con roles, nombre, email, etc.
redisTemplate.opsForValue().set(token, username, TOKEN_EXPIRATION_SEC, TimeUnit.SECONDS);
return token;
}
public boolean isValidToken(String token) {
String username = (String) redisTemplate.opsForValue().get(token);
return username != null; // El token existe y no ha expirado
}
public String getUsernameFromToken(String token) {
return (String) redisTemplate.opsForValue().get(token);
}
public void revokeToken(String token) {
// Eliminar token de Redis manualmente si se desea revocarlo
redisTemplate.delete(token);
}
public void revokeAllTokensForUser(String username) {
Set<String> keys = redisTemplate.keys("*"); // Buscar todas las claves
if (keys != null) {
for (String key : keys) {
String storedUsername = (String) redisTemplate.opsForValue().get(key);
if (username.equals(storedUsername)) {
redisTemplate.delete(key); // Elimina los tokens del usuario
}
}
}
}
}
Aquí, cada token se asocia con un username, pero en un sistema de e-commerce real incluiríamos más detalles (roles, ID del usuario, etc.). Cada vez que generamos un token, usamos un UUID y lo guardamos con un tiempo de expiración definido en Redis.
El filtro se encarga de interceptar cada solicitud HTTP, extraer el token (si existe) y validar su legitimidad. Solo si el token es válido, se marca al usuario como autenticado en el contexto de Spring Security.
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String token = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
}
if (token != null && tokenService.isValidToken(token)) {
String username = tokenService.getUsernameFromToken(token);
// Se podría cargar más info del usuario para asignar roles
UserDetails userDetails = new User(username, "",
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
// Spring Security requiere un objeto de Authentication para "loguear" al usuario
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// Se establece en el contexto de seguridad de Spring
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// Continúa la cadena de filtros
filterChain.doFilter(request, response);
}
}
La clase SecurityConfig utiliza la infraestructura de Spring Security para definir:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
TokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
return http
.csrf(csrf -> csrf.ignoringRequestMatchers("/auth/login")) // Mejor práctica
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/public/**").permitAll() // Sección pública
.anyRequest().authenticated()
)
.addFilterBefore(tokenAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Podríamos crear un controlador para manejar el login:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private TokenService tokenService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// Validar credenciales: esto podría implicar consultar una base de datos
// Suponemos un usuario "demo" con la contraseña "password" para el ejemplo.
if ("demo".equals(loginRequest.getUsername())
&& "password".equals(loginRequest.getPassword())) {
String token = tokenService.generateTokenForUser(loginRequest.getUsername());
return ResponseEntity.ok(Collections.singletonMap("token", token));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Credenciales inválidas");
}
}
}
class LoginRequest {
private String username;
private String password;
// Getters y Setters
}
El diagrama muestra dos fases principales de la de la autenticación basada en tokens:
Fase 1: flujo de login
Fase 2: llamada a un recurso protegido
Este patrón es bastante directo y más fácil de implementar que otros, pero requiere gestionar con cuidado el almacenamiento de los tokens y su revocación. Si tu sistema de e-commerce requiere revocar tokens rápidamente (por ejemplo, cuentas bloqueadas), deberás tener un enfoque central o un broadcast que invalide tokens en todos los nodos.
OAuth es un protocolo de autorización que permite a los usuarios compartir recursos protegidos con aplicaciones de terceros sin ceder sus credenciales directamente. Con OAuth 2.0, se introdujeron varios flujos (grants), siendo uno de los más comunes el Authorization Code Grant, empleado cuando queremos delegar la autenticación en un proveedor externo (Google, Facebook, GitHub, etc.), o en un servidor de autorización que despleguemos (como Keycloak, Okta, etc.).
Nota: OAuth no es estrictamente un protocolo de autenticación (aunque se use como tal en “login con Google/Facebook”), sino de autorización. Para autenticación real basada en OAuth2, se usa OpenID Connect (OIDC), que añade una capa de identidad sobre OAuth2, proporcionando un ID Token con información del usuario.
En este último caso, en lugar de almacenar directamente la contraseña del usuario, redirigimos al usuario a la página de Google para que se autentique. Una vez que Google verifica la identidad, nos devuelve un código que intercambiamos por un access token. Con ese token, consultamos la API de Google para obtener información del perfil (nombre, email, foto, etc.). Acto seguido, creamos o encontramos al usuario en nuestra base de datos interna y le damos acceso.
La versión de Spring Security 5 (y superiores) simplifica mucho la configuración de OAuth2. Para integrarnos con Google, añadimos a pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
En el archivo application.yml (o application.properties), se define la información de registro (registration) y el proveedor (provider).
Un ejemplo con Google es:
spring:
security:
oauth2:
client:
registration:
google:
client-id: TU_CLIENT_ID_DE_GOOGLE
client-secret: TU_CLIENT_SECRET_DE_GOOGLE
scope:
- email
- profile
redirect-uri: "{baseUrl}/login/oauth2/code/google"
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
Una vez definidas las propiedades anteriores, creas una clase de configuración que extienda WebSecurityConfigurerAdapter (para versiones de Spring Security < 6.x) o bien uses la nueva aproximación declarativa (desde Spring Boot 3).
Aquí se muestra el enfoque tradicional:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// Autorizamos ciertos endpoints
.authorizeRequests()
.antMatchers("/", "/login", "/error").permitAll()
.anyRequest().authenticated()
.and()
// Configuramos el login con OAuth2
.oauth2Login()
.defaultSuccessUrl("/home")
.failureUrl("/login?error=true");
}
}
De esta forma, Spring Security gestiona el ciclo de OAuth:
En un diagrama de flujo sería algo así:
Podríamos tener un Authorization Server propio, como Keycloak, que maneje a la vez la autenticación y la autorización. En un entorno de microservicios, cada servicio podría:
En un e-commerce, esto es particularmente útil si pretendemos:
OAuth es poderoso y flexible, pero puede ser más complejo de configurar y mantener, sobre todo si necesitamos manejar nosotros mismos un servidor de autorización. Sin embargo, cuando requerimos integrarnos con proveedores externos o delegar la autenticación a servicios confiables (p. ej., Google), OAuth2 es un estándar prácticamente universal.
A continuación, veremos el detalle sobre JWT, ejemplo de arquitectura híbrida (OAuth2 + JWT), buenas prácticas y una comparativa bastante interesante.
Te leo en comentarios 👇.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Cuéntanos qué te parece.