SpringBoot
Tutorial SpringBoot: Seguridad JWT en API REST reactiva Spring WebFlux
Añade seguridad en Spring WebFlux en API REST reactivas con Spring Security mediante autenticación JWT para seguridad con tokens en aplicaciones reactivas con Reactor.
Aprende SpringBoot GRATIS y certifícateAgregar dependencias JWT
En el pom.xml o gradle hay que agregar las siguientes dependencias:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
</dependency>
Crear entidades y registro
Para establecer un sistema de autenticación y autorización robusto en nuestra aplicación reactiva con Spring Boot 3, es fundamental definir correctamente las entidades User
, Role
y Product
. Estas entidades nos permitirán gestionar los usuarios, sus roles y los productos asociados a cada usuario. A continuación, se detallan las implementaciones de estas entidades y se muestra cómo crear un método de registro en un controlador reactivo para nuevos usuarios.
Comenzamos definiendo la entidad Role
, que representará los roles o perfiles de usuario dentro del sistema:
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
// Getters y setters
}
La entidad Role
contiene un campo name
único que identifica el nombre del rol. Ahora, definimos la entidad User
, estableciendo una relación ManyToMany con Role
:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// Getters y setters
}
En User
, utilizamos @ManyToMany
para indicar que un usuario puede tener múltiples roles y un rol puede pertenecer a múltiples usuarios. La anotación @JoinTable
especifica la tabla intermedia users_roles
que almacena las asociaciones.
Procedemos a definir la entidad Product
, que tiene una relación ManyToOne con User
, indicando que cada producto tiene un único autor:
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private BigDecimal price;
@ManyToOne
@JoinColumn(name = "author_id")
private User author;
// Getters y setters
}
En Product
, la anotación @ManyToOne
indica que varios productos pueden ser creados por un mismo usuario. El campo author
de tipo User
establece esta relación.
Para interactuar con estas entidades de forma reactiva, utilizamos repositorios que extienden ReactiveCrudRepository
. Creamos el UserRepository
:
public interface UserRepository extends R2dbcRepository<User, Long> {
Mono<User> findByUsername(String username);
}
El método findByUsername
nos permitirá buscar usuarios por su nombre de usuario de manera reactiva. De forma similar, creamos el RoleRepository
:
public interface RoleRepository extends R2dbcRepository<Role, Long> {
Mono<Role> findByName(String name);
}
Y el ProductRepository
para gestionar los productos:
public interface ProductRepository extends R2dbcRepository<Product, Long> {
// Métodos adicionales si es necesario
}
Ahora, implementamos un método de registro en un controlador reactivo para permitir que nuevos usuarios se registren en el sistema:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
public AuthController(UserRepository userRepository, RoleRepository roleRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/register")
public Mono<ResponseEntity<User>> register(@RequestBody User user) {
return userRepository.findByUsername(user.getUsername())
.flatMap(existingUser -> Mono.error(new UsernameAlreadyExistsException("El nombre de usuario ya existe")))
.switchIfEmpty(roleRepository.findByName("USER")
.defaultIfEmpty(new Role("USER"))
.flatMap(role -> {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.getRoles().add(role);
return userRepository.save(user);
})
)
.map(savedUser -> ResponseEntity.status(HttpStatus.CREATED).body(savedUser));
}
}
En este controlador:
- Inyectamos
UserRepository
,RoleRepository
yPasswordEncoder
para manejar la creación de usuarios y el cifrado de contraseñas. - El método
register
recibe un objetoUser
en el cuerpo de la petición. - Verificamos si el nombre de usuario ya existe utilizando
findByUsername
. - Si el usuario no existe, buscamos el rol "USER". Si no existe, lo creamos con
defaultIfEmpty
. - Asignamos el rol al usuario, codificamos la contraseña y guardamos el usuario en la base de datos.
- Devolvemos una respuesta con estado HTTP 201 (Created) y el usuario registrado.
Es importante manejar adecuadamente las secuencias reactivas utilizando operadores como flatMap
, switchIfEmpty
y map
para controlar el flujo de datos asíncronos.
Para garantizar la consistencia de los datos y evitar problemas de concurrencia, es esencial utilizar transacciones reactivas cuando se realicen operaciones que involucren múltiples entidades o repositorios.
Método de login reactivo
Para implementar un sistema de autenticación en una aplicación reactiva utilizando Spring Boot 3 y Spring WebFlux, es esencial crear un método de login que valide las credenciales de los usuarios y genere un token JWT. Este token incluirá claims y scopes basados en los roles asignados al usuario, permitiendo un control de acceso granular en la aplicación.
A continuación, se detallará cómo implementar este método utilizando la librería jjwt-api 0.12.6.
Comenzamos definiendo un controlador que manejará las solicitudes de autenticación:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
this.authenticationManager = authenticationManager;
this.jwtTokenProvider = jwtTokenProvider;
}
@PostMapping("/login")
public Mono<ResponseEntity<Map<String, Object>>> login(@RequestBody AuthRequest authRequest) {
return authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()))
.flatMap(authentication -> {
String token = jwtTokenProvider.generateToken(authentication);
Map<String, Object> response = new HashMap<>();
response.put("token", token);
return Mono.just(ResponseEntity.ok(response));
})
.switchIfEmpty(Mono.defer(() -> Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build())));
}
}
En este código:
- Se inyecta el
AuthenticationManager
para autenticar al usuario de forma reactiva. - El método
login
recibe unAuthRequest
con las credenciales del usuario. - Si la autenticación es exitosa, se genera un token JWT utilizando
jwtTokenProvider
. - Se construye una respuesta JSON con el token generado.
La clase AuthRequest
es un DTO sencillo que contiene el nombre de usuario y la contraseña:
public class AuthRequest {
private String username;
private String password;
// Getters y setters
}
El componente clave es JwtTokenProvider
, que genera el token JWT incorporando los claims y scopes necesarios:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration-ms}")
private long jwtExpirationMs;
public String generateToken(Authentication authentication) {
User principal = (User) authentication.getPrincipal();
Claims claims = Jwts.claims().setSubject(principal.getUsername());
claims.put("roles", principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
.compact();
}
}
En JwtTokenProvider
:
- Se utilizan las propiedades
jwt.secret
yjwt.expiration-ms
para configurar la clave secreta y la expiración del token. - Al generar el token, se establecen los claims estándar como el
subject
(nombre de usuario) y se agregan claims personalizados como losroles
. - Se firma el token con la clave secreta utilizando el algoritmo HS256.
Es importante definir las propiedades JWT en el archivo de configuración de la aplicación:
jwt:
secret: MiClaveSecretaSuperSegura12345
expiration-ms: 3600000
Para que el AuthenticationManager
funcione correctamente, necesitamos configurar un ReactiveUserDetailsService
que cargue los detalles del usuario desde el repositorio:
@Service
public class CustomUserDetailsService implements ReactiveUserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Mono<UserDetails> findByUsername(String username) {
return userRepository.findByUsername(username)
.map(user -> new User(user.getUsername(), user.getPassword(), getAuthorities(user)));
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
En CustomUserDetailsService
:
- Se carga el usuario por nombre de usuario de forma reactiva desde
UserRepository
. - Se transforman los roles del usuario en una colección de
GrantedAuthority
para ser utilizados por Spring Security.
La configuración de seguridad es esencial para definir cómo se manejan las autenticaciones:
@EnableWebFluxSecurity
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public SecurityConfig(CustomUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/auth/login").permitAll()
.anyExchange().authenticated()
)
.authenticationManager(authenticationManager())
.build();
}
@Bean
public AuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authManager.setPasswordEncoder(passwordEncoder);
return authManager;
}
}
En esta configuración:
- Se deshabilita CSRF, ya que no es necesario para APIs REST.
- Se permite acceso público al endpoint
/api/auth/login
. - Se requiere autenticación para cualquier otra ruta.
- Se define un
AuthenticationManager
que utiliza elCustomUserDetailsService
.
El PasswordEncoder
debe estar definido como un bean para cifrar y verificar las contraseñas:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
Al utilizar PasswordEncoderFactories.createDelegatingPasswordEncoder()
, se asegura la compatibilidad con diferentes formatos de contraseña y algoritmos de cifrado.
Es crucial que las contraseñas almacenadas en la base de datos estén cifradas. Al momento del registro, como se vio en secciones anteriores, se debe utilizar el passwordEncoder
para cifrar la contraseña del usuario antes de guardarla.
En cuanto a los scopes y claims adicionales, si se requiere incluir información extra en el token JWT, basta con agregarlos al objeto claims
en el JwtTokenProvider
:
claims.put("scopes", List.of("ROLE_USER", "ROLE_ADMIN"));
claims.put("customClaim", "valorPersonalizado");
Estos claims pueden ser utilizados posteriormente para realizar validaciones adicionales en las solicitudes a la API.
La librería jjwt-api 0.12.6 nos ofrece una API sencilla y potente para la creación y manejo de tokens JWT. Al utilizarla, es importante asegurar que se están siguiendo las mejores prácticas de seguridad, como:
- Utilizar una clave secreta robusta y almacenada de forma segura.
- Establecer una expiración adecuada del token para minimizar riesgos.
- Firmar el token con un algoritmo seguro y recomendado.
Finalmente, es importante recordar que el token JWT generado debe ser incluido por el cliente en las cabeceras de las peticiones siguientes, generalmente en la cabecera Authorization
con el prefijo Bearer
:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Con este método de login reactivo, hemos implementado un sistema de autenticación que genera tokens JWT con claims y scopes basados en los roles del usuario, utilizando la librería jjwt-api 0.12.6. Esto permite asegurar nuestra API REST de manera eficiente en aplicaciones reactivas con Spring WebFlux.
Creación de filtro JWT
Para asegurar nuestras APIs REST reactivas utilizando JWT en Spring Boot 3, es esencial implementar un filtro que verifique el token presente en la cabecera Authorization de cada petición. Este filtro validará el token, extraerá la información del usuario y establecerá la autenticación en el contexto de seguridad. A continuación, se detalla cómo crear un filtro JWT reactivo y cómo configurar el SecurityWebFilterChain
para integrarlo en la aplicación.
Comenzamos creando una clase que implemente la interfaz WebFilter
. Esta clase actuará como nuestro filtro JWT:
@Component
public class JwtAuthenticationFilter implements WebFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String token = extractToken(exchange.getRequest());
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
return chain.filter(exchange);
}
private String extractToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
En este filtro:
- Utilizamos el
JwtTokenProvider
para validar el token y obtener la autenticación. - El método
filter
se encarga de interceptar cada petición y extraer el token de la cabecera Authorization. - Si el token es válido, se establece la autenticación en el contexto de seguridad utilizando
ReactiveSecurityContextHolder
.
Es importante notar que, al ser una aplicación reactiva, debemos manejar el contexto de seguridad de manera adecuada. En lugar de usar el enfoque tradicional de sincronía, empleamos contextWrite
para asociar la autenticación al Security Context reactivo.
El método extractToken
obtiene el token JWT de la cabecera Authorization, verificando que siga el esquema Bearer.
El JwtTokenProvider
debe proporcionar los métodos validateToken
y getAuthentication
:
@Component
public class JwtTokenProvider {
private final String jwtSecret;
private final JwtParser jwtParser;
public JwtTokenProvider(@Value("${jwt.secret}") String jwtSecret) {
this.jwtSecret = jwtSecret;
this.jwtParser = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)))
.build();
}
public boolean validateToken(String token) {
try {
jwtParser.parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
public Authentication getAuthentication(String token) {
Claims claims = jwtParser.parseClaimsJws(token).getBody();
String username = claims.getSubject();
List<SimpleGrantedAuthority> authorities = ((List<?>) claims.get("roles")).stream()
.map(role -> new SimpleGrantedAuthority((String) role))
.collect(Collectors.toList());
UserDetails userDetails = User.withUsername(username)
.authorities(authorities)
.password("") // La contraseña no es necesaria aquí
.build();
return new UsernamePasswordAuthenticationToken(userDetails, token, authorities);
}
}
En este proveedor de tokens:
- Inicializamos el
JwtParser
con la clave secreta obtenida de las propiedades de configuración. - El método
validateToken
intenta analizar el token; si es exitoso, devuelve true, de lo contrario, false. - El método
getAuthentication
extrae los claims del token, obtiene el nombre de usuario y los roles, y construye un objetoAuthentication
.
Al manejar los roles, extraemos la lista de roles del claim roles
y los convertimos en una colección de authorities que Spring Security pueda entender.
Ahora, configuramos el SecurityWebFilterChain
para incluir nuestro filtro:
@EnableWebFluxSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationManager authenticationManager;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
AuthenticationManager authenticationManager) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.authenticationManager = authenticationManager;
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.authenticationManager(authenticationManager)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/auth/**").permitAll()
.anyExchange().authenticated()
)
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
}
En esta configuración:
- Deshabilitamos CSRF, HTTP Basic y form login, ya que utilizamos JWT para la autenticación.
- Permitimos el acceso sin autenticar a los endpoints bajo
/api/auth/**
, como el de login y registro. - Requerimos autenticación para cualquier otra ruta.
- Añadimos nuestro
JwtAuthenticationFilter
en el orden de filtros de AUTHENTICATION.
Es esencial registrar el filtro en el orden correcto para que se ejecute en el momento apropiado durante el procesamiento de la petición.
Además, el AuthenticationManager
debe estar configurado para autenticar al usuario si es necesario. Sin embargo, en este caso, el JwtAuthenticationFilter
ya establece la autenticación basándose en el token, por lo que el AuthenticationManager
se utiliza principalmente durante el proceso de login.
Para completar, aseguramos que el filtro sea detectado por Spring al marcarlo con @Component
. De esta forma, será inyectado automáticamente donde se requiera.
Es importante también manejar adecuadamente las excepciones y posibles errores en el JwtAuthenticationFilter
. Por ejemplo, podríamos modificar el método filter
para manejar tokens expirados o inválidos:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String token = extractToken(exchange.getRequest());
if (token != null) {
try {
if (jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
} catch (JwtException e) {
return Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token inválido"));
}
}
return chain.filter(exchange);
}
De esta manera, si el token es inválido o ha expirado, se responde con un estado HTTP 401 Unauthorized.
Al configurar el SecurityWebFilterChain
y el filtro JWT, aseguramos que todas las peticiones a nuestra API estén protegidas. Solo los usuarios autenticados con un token válido podrán acceder a los recursos protegidos.
Es fundamental también mantener la clave secreta del JWT de forma segura y no exponerla en el código. Por ello, utilizamos la anotación @Value
para inyectarla desde las propiedades de configuración.
Finalmente, recordemos que en el proceso de generación del token durante el login, es importante incluir los roles en los claims, para que puedan ser extraídos y utilizados por el filtro:
claims.put("roles", principal.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toList()));
Esto garantiza que, al validar el token, podamos reconstruir las autoridades y asignarlas al contexto de seguridad.
Con esta implementación, hemos creado un filtro JWT reactivo que verifica el token presente en la cabecera Authorization y configurado el SecurityWebFilterChain
para integrar el filtro en el flujo de seguridad de la aplicación. Esto nos proporciona un mecanismo robusto y eficiente para proteger nuestras APIs reactivas con Spring WebFlux y Spring Security.
Protección de rutas
Para implementar una seguridad basada en roles en una aplicación reactiva con Spring Boot 3, es fundamental configurar el SecurityWebFilterChain
de forma adecuada. Esto nos permitirá restringir el acceso a ciertas rutas según los roles asignados a los usuarios. Además, al crear nuevas entidades, como Producto
, es necesario asignar el usuario autenticado como autor, lo cual se logra utilizando la anotación @AuthenticationPrincipal
.
En primer lugar, configuramos el SecurityWebFilterChain
para definir las reglas de acceso basadas en roles. A continuación se muestra cómo podemos hacerlo:
@EnableWebFluxSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationManager authenticationManager;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
AuthenticationManager authenticationManager) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.authenticationManager = authenticationManager;
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.authenticationManager(authenticationManager)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/auth/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/productos/**").hasRole("USER")
.pathMatchers("/api/admin/**").hasRole("ADMIN")
.anyExchange().authenticated()
)
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
}
En esta configuración:
- Se permite el acceso público a los endpoints bajo
/api/auth/**
, que incluyen el login y registro. - Se restringe el acceso a las rutas que comienzan con
/api/productos/**
para que solo usuarios con el rolUSER
puedan realizar peticiones POST, es decir, crear nuevos productos. - Las rutas bajo
/api/admin/**
están reservadas para usuarios con el rolADMIN
. - Cualquier otra ruta requiere autenticación.
Es importante notar que usamos hasRole("USER")
y hasRole("ADMIN")
para especificar los roles necesarios para acceder a ciertas rutas. La función pathMatchers
nos permite definir rutas específicas y métodos HTTP para un control más preciso.
Además, al trabajar con roles en Spring Security, debemos asegurarnos de que los roles estén prefijados correctamente. Por defecto, hasRole
agrega el prefijo ROLE_
, por lo que si nuestro rol en la base de datos es USER
, Spring lo interpretará como ROLE_USER
.
A continuación, implementamos el controlador reactivo ProductoController
, donde utilizamos @AuthenticationPrincipal
para obtener el usuario autenticado y asignarlo como autor del producto:
@RestController
@RequestMapping("/api/productos")
public class ProductoController {
private final ProductoRepository productoRepository;
public ProductoController(ProductoRepository productoRepository) {
this.productoRepository = productoRepository;
}
@PostMapping
public Mono<ResponseEntity<Producto>> crearProducto(@RequestBody Producto producto,
@AuthenticationPrincipal Mono<UserDetails> usuarioActual) {
return usuarioActual
.map(user-> {
// Aquí asignamos el usuario autenticado como autor
producto.setAutor(user);
return productoRepository.save(producto);
})
.map(productoGuardado -> ResponseEntity.status(HttpStatus.CREATED).body(productoGuardado));
}
}
En este controlador:
- Definimos el método
crearProducto
que responde a las peticiones POST en/api/productos
. - Utilizamos
@AuthenticationPrincipal
para obtener unMono<UserDetails>
del usuario autenticado de forma reactiva. - Extraemos el nombre de usuario mediante
UserDetails::getUsername
y lo asignamos al campoautor
del producto. - Guardamos el producto en el repositorio y retornamos una respuesta con estado 201 Created.
Es crucial manejar la asincronía y la naturaleza reactiva de nuestros componentes. Por ello, trabajamos con Mono
y utilizamos operadores como flatMap
y map
para gestionar el flujo de datos.
Es importante asegurarse de que el usuario tenga el rol adecuado para crear productos. En nuestra configuración de seguridad, ya hemos especificado que solo los usuarios con el rol USER
pueden acceder al endpoint POST /api/productos/**
.
Si deseamos agregar protección adicional dentro del propio controlador, podemos utilizar anotaciones como @PreAuthorize
. Sin embargo, según las instrucciones, no debemos utilizar esas anotaciones en esta lección.
Para probar que las restricciones de acceso funcionan correctamente, podemos crear dos usuarios con roles distintos:
- Un usuario con rol
USER
:
INSERT INTO roles (name) VALUES ('USER');
INSERT INTO users (username, password) VALUES ('usuario1', '{bcrypt}$2a$10$xyz...');
INSERT INTO users_roles (user_id, role_id) VALUES (1, 1);
- Un usuario con rol
ADMIN
:
INSERT INTO roles (name) VALUES ('ADMIN');
INSERT INTO users (username, password) VALUES ('admin', '{bcrypt}$2a$10$abc...');
INSERT INTO users_roles (user_id, role_id) VALUES (2, 2);
Luego, al autenticar y obtener el token JWT para cada usuario, podemos realizar peticiones al endpoint /api/productos
:
- Si utilizamos el token del usuario con rol
USER
, deberíamos poder crear un nuevo producto exitosamente. - Si intentamos crear un producto con el token del usuario
ADMIN
, la petición debería ser rechazada con un error 403 Forbidden, ya que el rolADMIN
no tiene permiso para esa ruta según nuestra configuración.
Este comportamiento demuestra cómo las reglas definidas en el SecurityWebFilterChain
controlan el acceso basado en roles de manera efectiva.
Además, si queremos proteger otras rutas, podemos agregar más reglas en el método authorizeExchange
:
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/auth/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/productos/**").hasRole("USER")
.pathMatchers(HttpMethod.DELETE, "/api/productos/**").hasRole("ADMIN")
.anyExchange().authenticated()
)
En este ejemplo, solo los usuarios con rol ADMIN
pueden eliminar productos mediante peticiones DELETE.
En resumen, la protección de rutas basada en roles se implementa configurando adecuadamente el SecurityWebFilterChain
en Spring Security. Por otro lado, el uso de @AuthenticationPrincipal
en controladores reactivos nos permite acceder al usuario autenticado y utilizar su información al crear o manipular entidades, asegurando que las operaciones se realicen en el contexto del usuario correcto.
Es fundamental seguir las mejores prácticas al manejar seguridad en aplicaciones web, asegurando que los datos sensibles estén protegidos y que el acceso a los recursos esté correctamente controlado según los roles y permisos de los usuarios.
Ejercicios de esta lección Seguridad JWT en API REST reactiva Spring WebFlux
Evalúa tus conocimientos de esta lección Seguridad JWT en API REST reactiva Spring WebFlux con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
API Query By Example (QBE)
Identificadores y relaciones JPA
Borrar datos de base de datos
Web y Test Starters
Métodos find en repositorios
Controladores Spring MVC
Inserción de datos
CRUD Customers Spring MVC + Spring Data JPA
Backend API REST con Spring Boot
Controladores Spring REST
Uso de Spring con Thymeleaf
API Specification
Registro de usuarios
Crear entidades JPA
Asociaciones en JPA
Asociaciones de entidades JPA
Integración con Vue
Consultas JPQL
Open API y cómo agregarlo en Spring Boot
Uso de Controladores REST
Repositorios reactivos
Inyección de dependencias
Introducción a Spring Boot
CRUD y JPA Repository
Inyección de dependencias
Vista en Spring MVC con Thymeleaf
Servicios en Spring
Operadores Reactivos
Configuración de Vue
Entidades JPA
Integración con Angular
API Specification
API Query By Example (QBE)
Controladores MVC
Anotaciones y mapeo en JPA
Consultas JPQL con @Query en Spring Data JPA
Repositorios Spring Data
Inyección de dependencias
Data JPA y Mail Starters
Configuración de Angular
Controladores Spring REST
Configuración de Controladores MVC
Consultas JPQL con @Query en Spring Data JPA
Actualizar datos de base de datos
Verificar token JWT en peticiones
Login de usuarios
Integración con React
Configuración de React
Todas las lecciones de SpringBoot
Accede a todas las lecciones de SpringBoot y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Spring Boot
Introducción Y Entorno
Spring Boot Starters
Introducción Y Entorno
Inyección De Dependencias
Introducción Y Entorno
Controladores Spring Mvc
Spring Web
Vista En Spring Mvc Con Thymeleaf
Spring Web
Controladores Spring Rest
Spring Web
Open Api Y Cómo Agregarlo En Spring Boot
Spring Web
Servicios En Spring
Spring Web
Clientes Resttemplate Y Restclient
Spring Web
Rxjava En Spring Web
Spring Web
Crear Entidades Jpa
Persistencia Spring Data
Asociaciones De Entidades Jpa
Persistencia Spring Data
Repositorios Spring Data
Persistencia Spring Data
Métodos Find En Repositorios
Persistencia Spring Data
Inserción De Datos
Persistencia Spring Data
Actualizar Datos De Base De Datos
Persistencia Spring Data
Borrar Datos De Base De Datos
Persistencia Spring Data
Consultas Jpql Con @Query En Spring Data Jpa
Persistencia Spring Data
Api Query By Example (Qbe)
Persistencia Spring Data
Api Specification
Persistencia Spring Data
Repositorios Reactivos
Persistencia Spring Data
Introducción E Instalación De Apache Kafka
Mensajería Asíncrona
Crear Proyecto Con Apache Kafka
Mensajería Asíncrona
Creación De Producers
Mensajería Asíncrona
Creación De Consumers
Mensajería Asíncrona
Kafka Streams En Spring Boot
Mensajería Asíncrona
Introducción A Spring Webflux
Reactividad Webflux
Spring Data R2dbc
Reactividad Webflux
Controlador Rest Reactivo Basado En Anotaciones
Reactividad Webflux
Controlador Rest Reactivo Funcional
Reactividad Webflux
Operadores Reactivos Básicos
Reactividad Webflux
Operadores Reactivos Avanzados
Reactividad Webflux
Cliente Reactivo Webclient
Reactividad Webflux
Introducción A Spring Security
Seguridad Con Spring Security
Seguridad Basada En Formulario En Mvc Con Thymeleaf
Seguridad Con Spring Security
Registro De Usuarios
Seguridad Con Spring Security
Login De Usuarios
Seguridad Con Spring Security
Verificar Token Jwt En Peticiones
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Spring Web
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Reactiva Spring Webflux
Seguridad Con Spring Security
Autenticación Y Autorización Con Anotaciones
Seguridad Con Spring Security
Testing Unitario De Componentes Y Servicios
Testing Con Spring Test
Testing De Repositorios Spring Data Jpa Y Acceso A Datos Con Spring Test
Testing Con Spring Test
Testing Controladores Spring Mvc Con Thymeleaf
Testing Con Spring Test
Testing Controladores Rest Con Json
Testing Con Spring Test
Testing De Aplicaciones Reactivas Webflux
Testing Con Spring Test
Testing De Seguridad Spring Security
Testing Con Spring Test
Testing Con Apache Kafka
Testing Con Spring Test
Integración Con Angular
Integración Frontend
Integración Con React
Integración Frontend
Integración Con Vue
Integración Frontend
En esta lección
Objetivos de aprendizaje de esta lección
- Aprender qué es JWT
- Agregar las dependencias JWT al proyecto Spring Boot
- Creación de método de registro de usuarios
- Creación de método de login de usuarios
- Creación de filtro de verificación de token JWT de usuario
- Seguridad en rutas de la aplicación en controladores REST