Spring Boot

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ícate

Agregar 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 y PasswordEncoder para manejar la creación de usuarios y el cifrado de contraseñas.
  • El método register recibe un objeto User 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 un AuthRequest 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 y jwt.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 los roles.
  • 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 el CustomUserDetailsService.

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 objeto Authentication.

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 rol USER puedan realizar peticiones POST, es decir, crear nuevos productos.
  • Las rutas bajo /api/admin/** están reservadas para usuarios con el rol ADMIN.
  • 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 un Mono<UserDetails> del usuario autenticado de forma reactiva.
  • Extraemos el nombre de usuario mediante UserDetails::getUsername y lo asignamos al campo autor 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:

  1. 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);
  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 rol ADMIN 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.

Aprende SpringBoot GRATIS online

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)

Spring Boot
Test

Identificadores y relaciones JPA

Spring Boot
Puzzle

Borrar datos de base de datos

Spring Boot
Test

Web y Test Starters

Spring Boot
Puzzle

Métodos find en repositorios

Spring Boot
Test

Controladores Spring MVC

Spring Boot
Código

Inserción de datos

Spring Boot
Test

CRUD Customers Spring MVC + Spring Data JPA

Spring Boot
Proyecto

Backend API REST con Spring Boot

Spring Boot
Proyecto

Controladores Spring REST

Spring Boot
Código

Uso de Spring con Thymeleaf

Spring Boot
Puzzle

API Specification

Spring Boot
Puzzle

Registro de usuarios

Spring Boot
Test

Crear entidades JPA

Spring Boot
Código

Asociaciones en JPA

Spring Boot
Test

Asociaciones de entidades JPA

Spring Boot
Código

Integración con Vue

Spring Boot
Test

Consultas JPQL

Spring Boot
Código

Open API y cómo agregarlo en Spring Boot

Spring Boot
Puzzle

Uso de Controladores REST

Spring Boot
Puzzle

Repositorios reactivos

Spring Boot
Test

Inyección de dependencias

Spring Boot
Test

Introducción a Spring Boot

Spring Boot
Test

CRUD y JPA Repository

Spring Boot
Puzzle

Inyección de dependencias

Spring Boot
Código

Vista en Spring MVC con Thymeleaf

Spring Boot
Test

Servicios en Spring

Spring Boot
Código

Operadores Reactivos

Spring Boot
Puzzle

Configuración de Vue

Spring Boot
Puzzle

Entidades JPA

Spring Boot
Test

Integración con Angular

Spring Boot
Test

API Specification

Spring Boot
Test

API Query By Example (QBE)

Spring Boot
Puzzle

Controladores MVC

Spring Boot
Test

Anotaciones y mapeo en JPA

Spring Boot
Puzzle

Consultas JPQL con @Query en Spring Data JPA

Spring Boot
Test

Repositorios Spring Data

Spring Boot
Test

Inyección de dependencias

Spring Boot
Puzzle

Data JPA y Mail Starters

Spring Boot
Test

Configuración de Angular

Spring Boot
Puzzle

Controladores Spring REST

Spring Boot
Test

Configuración de Controladores MVC

Spring Boot
Puzzle

Consultas JPQL con @Query en Spring Data JPA

Spring Boot
Puzzle

Actualizar datos de base de datos

Spring Boot
Test

Verificar token JWT en peticiones

Spring Boot
Test

Login de usuarios

Spring Boot
Test

Integración con React

Spring Boot
Test

Configuración de React

Spring Boot
Puzzle

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

Spring Boot

Introducción Y Entorno

Spring Boot Starters

Spring Boot

Introducción Y Entorno

Inyección De Dependencias

Spring Boot

Introducción Y Entorno

Controladores Spring Mvc

Spring Boot

Spring Web

Vista En Spring Mvc Con Thymeleaf

Spring Boot

Spring Web

Controladores Spring Rest

Spring Boot

Spring Web

Open Api Y Cómo Agregarlo En Spring Boot

Spring Boot

Spring Web

Servicios En Spring

Spring Boot

Spring Web

Clientes Resttemplate Y Restclient

Spring Boot

Spring Web

Rxjava En Spring Web

Spring Boot

Spring Web

Crear Entidades Jpa

Spring Boot

Persistencia Spring Data

Asociaciones De Entidades Jpa

Spring Boot

Persistencia Spring Data

Repositorios Spring Data

Spring Boot

Persistencia Spring Data

Métodos Find En Repositorios

Spring Boot

Persistencia Spring Data

Inserción De Datos

Spring Boot

Persistencia Spring Data

Actualizar Datos De Base De Datos

Spring Boot

Persistencia Spring Data

Borrar Datos De Base De Datos

Spring Boot

Persistencia Spring Data

Consultas Jpql Con @Query En Spring Data Jpa

Spring Boot

Persistencia Spring Data

Api Query By Example (Qbe)

Spring Boot

Persistencia Spring Data

Api Specification

Spring Boot

Persistencia Spring Data

Repositorios Reactivos

Spring Boot

Persistencia Spring Data

Introducción E Instalación De Apache Kafka

Spring Boot

Mensajería Asíncrona

Crear Proyecto Con Apache Kafka

Spring Boot

Mensajería Asíncrona

Creación De Producers

Spring Boot

Mensajería Asíncrona

Creación De Consumers

Spring Boot

Mensajería Asíncrona

Kafka Streams En Spring Boot

Spring Boot

Mensajería Asíncrona

Introducción A Spring Webflux

Spring Boot

Reactividad Webflux

Spring Data R2dbc

Spring Boot

Reactividad Webflux

Controlador Rest Reactivo Basado En Anotaciones

Spring Boot

Reactividad Webflux

Controlador Rest Reactivo Funcional

Spring Boot

Reactividad Webflux

Operadores Reactivos Básicos

Spring Boot

Reactividad Webflux

Operadores Reactivos Avanzados

Spring Boot

Reactividad Webflux

Cliente Reactivo Webclient

Spring Boot

Reactividad Webflux

Introducción A Spring Security

Spring Boot

Seguridad Con Spring Security

Seguridad Basada En Formulario En Mvc Con Thymeleaf

Spring Boot

Seguridad Con Spring Security

Registro De Usuarios

Spring Boot

Seguridad Con Spring Security

Login De Usuarios

Spring Boot

Seguridad Con Spring Security

Verificar Token Jwt En Peticiones

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Spring Web

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Reactiva Spring Webflux

Spring Boot

Seguridad Con Spring Security

Autenticación Y Autorización Con Anotaciones

Spring Boot

Seguridad Con Spring Security

Testing Unitario De Componentes Y Servicios

Spring Boot

Testing Con Spring Test

Testing De Repositorios Spring Data Jpa Y Acceso A Datos Con Spring Test

Spring Boot

Testing Con Spring Test

Testing Controladores Spring Mvc Con Thymeleaf

Spring Boot

Testing Con Spring Test

Testing Controladores Rest Con Json

Spring Boot

Testing Con Spring Test

Testing De Aplicaciones Reactivas Webflux

Spring Boot

Testing Con Spring Test

Testing De Seguridad Spring Security

Spring Boot

Testing Con Spring Test

Testing Con Apache Kafka

Spring Boot

Testing Con Spring Test

Integración Con Angular

Spring Boot

Integración Frontend

Integración Con React

Spring Boot

Integración Frontend

Integración Con Vue

Spring Boot

Integración Frontend

Accede GRATIS a SpringBoot y certifícate

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