Spring Boot

SpringBoot

Tutorial SpringBoot: Seguridad JWT en API REST Spring Web

Aprende a agregar autenticación basada en JWT con Spring Security en aplicaciones de Spring Boot con controladores API REST y SecurityFIlterChain.

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 de usuarios

En el desarrollo de una API REST segura con Spring Boot 3, es fundamental definir correctamente las entidades que representarán a los usuarios, sus roles y los productos gestionados. A continuación, se detallan los pasos para crear las entidades User, Role y Product, estableciendo las relaciones necesarias entre ellas y proporcionando un método de registro para nuevos usuarios.

  1. Entidad Role

La entidad Role representa los diferentes roles que un usuario puede tener en el sistema. Es esencial para implementar la autorización basada en roles.

@Entity
@Table(name = "roles")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String name;

    // Constructores, getters y setters
}

En esta entidad, el campo name es único y no puede ser nulo, asegurando que cada rol sea distintivo dentro del sistema.

  1. Entidad User

La entidad User representa a los usuarios del sistema y establece una relación ManyToMany con Role, ya que un usuario puede tener múltiples roles y un rol puede pertenecer a varios usuarios.

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    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<>();

    // Constructores, getters y setters
}

El uso de @ManyToMany y @JoinTable configura la relación muchos a muchos entre usuarios y roles, y establece la tabla intermedia users_roles.

  1. Entidad Product

La entidad Product representa los productos creados por los usuarios. Establece una relación ManyToOne con User, indicando que un producto tiene un único autor, pero un usuario puede ser autor de múltiples productos.

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    private String description;

    @ManyToOne
    @JoinColumn(name = "author_id", nullable = false)
    private User author;

    // Constructores, getters y setters
}

La anotación @ManyToOne y @JoinColumn establecen la relación con el autor, garantizando que cada producto esté asociado a un usuario existente.

  1. Repositorio UserRepository

Para interactuar con la base de datos, se crea el repositorio UserRepository, que permite operaciones CRUD y búsquedas específicas.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

El método findByUsername es crucial para la autenticación, permitiendo encontrar usuarios por su nombre de usuario.

  1. Servicio de Usuario

Se implementa un servicio para manejar la lógica de negocio relacionada con los usuarios, incluyendo el registro de nuevos usuarios y la gestión de sus roles.

@Service
public class UserService {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository; // Asumiendo que existe un RoleRepository
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository,
                       RoleRepository roleRepository,
                       PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public User registerUser(User user) {
        if (userRepository.findByUsername(user.getUsername()).isPresent()) {
            throw new IllegalArgumentException("El nombre de usuario ya está en uso");
        }

        user.setPassword(passwordEncoder.encode(user.getPassword()));

        Role userRole = roleRepository.findByName("USER")
            .orElseThrow(() -> new RuntimeException("Rol USER no encontrado"));
        user.getRoles().add(userRole);

        return userRepository.save(user);
    }
}

Este servicio asegura que las contraseñas sean codificadas y que los usuarios nuevos tengan asignado un rol por defecto.

  1. Controlador de Autenticación

El controlador expone un endpoint para el registro de usuarios, permitiendo que nuevos usuarios se registren en el sistema.

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final UserService userService;

    public AuthController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/register")
    public ResponseEntity<User> register(@RequestBody @Valid User user) {
        User registeredUser = userService.registerUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(registeredUser);
    }
}

El uso de @Valid en el parámetro asegura que los datos del usuario sean validados antes de procesarlos.

  1. Validación de la Entidad User

Para garantizar la integridad de los datos, se añaden anotaciones de validación en la entidad User.

@Entity
@Table(name = "users")
public class User {

    // ...

    @NotBlank(message = "El nombre de usuario es obligatorio")
    @Size(min = 3, max = 50, message = "El nombre de usuario debe tener entre 3 y 50 caracteres")
    private String username;

    @NotBlank(message = "La contraseña es obligatoria")
    @Size(min = 6, message = "La contraseña debe tener al menos 6 caracteres")
    private String password;

    // ...
}

Estas anotaciones ayudan a proteger el sistema de datos incompletos o inválidos enviados por los usuarios.

  1. Configuración de Seguridad

Es necesario configurar Spring Security para permitir el acceso al endpoint de registro sin autenticación.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // ...

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/auth/register").permitAll()
                .anyRequest().authenticated()
            )
            // ...
            .csrf(csrf -> csrf.disable());

        return http.build();
    }
}

Esta configuración permite que los usuarios no autenticados puedan acceder al endpoint de registro, mientras que protege el resto de la aplicación.

  1. Codificador de Contraseñas

Para asegurar las contraseñas de los usuarios, se define un PasswordEncoder que utiliza BCrypt.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

El uso de BCrypt es una práctica recomendada para almacenar contraseñas de forma segura.

  1. Repositorio RoleRepository

Para completar el servicio de usuario, se necesita un repositorio para gestionar los roles.

public interface RoleRepository extends JpaRepository<Role, Long> {
    Optional<Role> findByName(String name);
}

Este repositorio permite buscar roles por su nombre, lo cual es esencial al asignar roles a los usuarios.

  1. Manejo de Excepciones

Para proporcionar respuestas significativas en caso de errores, se pueden implementar controladores de excepciones.

@RestControllerAdvice
public class ExceptionHandlerController {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body(ex.getMessage());
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntime(RuntimeException ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
    }
}

Este enfoque mejora la experiencia del usuario al proporcionar mensajes claros sobre lo que salió mal.

  1. Preparación de Datos Iniciales

Para facilitar el desarrollo, es útil crear datos iniciales, como roles predeterminados, utilizando un CommandLineRunner.

@Component
public class DataInitializer implements CommandLineRunner {

    private final RoleRepository roleRepository;

    public DataInitializer(RoleRepository roleRepository) {
        this.roleRepository = roleRepository;
    }

    @Override
    public void run(String... args) {
        if (roleRepository.findByName("USER").isEmpty()) {
            Role userRole = new Role();
            userRole.setName("USER");
            roleRepository.save(userRole);
        }

        if (roleRepository.findByName("ADMIN").isEmpty()) {
            Role adminRole = new Role();
            adminRole.setName("ADMIN");
            roleRepository.save(adminRole);
        }
    }
}

Este componente asegura que los roles necesarios existan en la base de datos al iniciar la aplicación.

  1. Relación entre Product y User

Cuando se crea un producto, es importante asociarlo con el usuario autenticado. En el controlador de productos, se puede obtener el usuario actual y asignarlo como autor.

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;
    private final AuthenticationFacade authenticationFacade;

    public ProductController(ProductService productService, 
                             AuthenticationFacade authenticationFacade) {
        this.productService = productService;
        this.authenticationFacade = authenticationFacade;
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody @Valid Product product) {
        User currentUser = authenticationFacade.getAuthenticatedUser();
        product.setAuthor(currentUser);
        Product savedProduct = productService.saveProduct(product);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
    }
}
  1. Acceso al usuario autenticado

Para obtener el usuario autenticado, se puede implementar una interfaz que proporcione este acceso.

@Component
public class AuthenticationFacade {

    private final UserRepository userRepository;

    public AuthenticationFacade(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getAuthenticatedUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
    }
}

Este componente centraliza la lógica para obtener el usuario actual, facilitando su reutilización en diferentes partes de la aplicación.

  1. Implementación del servicio ProductService

El servicio para productos maneja la lógica relacionada con la creación y gestión de productos.

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }

    // Otros métodos para actualizar, eliminar y obtener productos
}

Este servicio abstrae las operaciones de persistencia y permite aplicar reglas de negocio adicionales si es necesario.

Método de login para autenticación y creación de token JWT

Para implementar un método de login que permita la autenticación de usuarios y la generación de tokens JWT con scopes y claims basados en los roles, utilizaremos la librería jjwt-api. Este proceso es fundamental para asegurar que solo los usuarios autenticados puedan acceder a los recursos protegidos de nuestra API REST.

En primer lugar, es necesario crear un endpoint de autenticación que reciba las credenciales del usuario. Este endpoint estará en el controlador de autenticación y permitirá validar la identidad del usuario para posteriormente generar el token JWT.

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;

    public AuthController(AuthenticationManager authenticationManager,
                          JwtService jwtService) {
        this.authenticationManager = authenticationManager;
        this.jwtService = jwtService;
    }

    @PostMapping("/login")
    public ResponseEntity<TokenResponse> login(@RequestBody @Valid LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(),
                loginRequest.getPassword()
            )
        );

        User user = (User) authentication.getPrincipal();
        String token = jwtService.generateToken(user);

        return ResponseEntity.ok(new TokenResponse(token));
    }
}

En este código, el AuthenticationManager se encarga de autenticar al usuario utilizando las credenciales proporcionadas. Si la autenticación es exitosa, se obtiene el usuario autenticado y se genera el token JWT mediante el JwtService.

La clase LoginRequest representa el modelo de datos que recibe el endpoint, incluyendo las validaciones necesarias:

public class LoginRequest {

    @NotBlank(message = "El nombre de usuario es obligatorio")
    private String username;

    @NotBlank(message = "La contraseña es obligatoria")
    private String password;

    // Getters y setters
}

Es importante devolver una respuesta clara al cliente después de la autenticación. Para ello, se crea la clase TokenResponse, que contiene el token generado:

public class TokenResponse {

    private final String token;

    public TokenResponse(String token) {
        this.token = token;
    }

    public String getToken() {
        return token;
    }
}

El siguiente paso es implementar el JwtService, que será responsable de generar el token JWT incorporando los scopes y claims basados en los roles del usuario:

@Service
public class JwtService {

    private final JwtBuilder jwtBuilder;
    private final SecretKey secretKey; // Clave secreta para firmar el token

    public JwtService(SecretKey secretKey) {
        this.secretKey = secretKey;
        this.jwtBuilder = Jwts.builder();
    }

    public String generateToken(User user) {
        Instant now = Instant.now();
        Date expiryDate = Date.from(now.plus(1, ChronoUnit.HOURS));

        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", user.getRoles()
            .stream()
            .map(Role::getName)
            .collect(Collectors.toList()));
            
            return jwtBuilder
                // id del usuario
                .subject(String.valueOf(user.getId()))
                // La clave secreta para firmar el token y saber que es nuestro cuando lleguen las peticiones del frontend
                .signWith(secretKey, SignatureAlgorithm.HS256)
                // Fecha emisión del token
                .issuedAt(Date.from(now))
                // Fecha de expiración del token
                .expiration(expiryDate)
                // información personalizada: rol o roles, username, email, avatar...
                // .claim("role", user.getRole())
                .claim("email", user.getEmail())
                //.claim("avatar", user.getAvatarUrl())
                // Construye el token
                .compact();
                
                
    }
}

En este servicio, utilizamos el JwtBuilder proporcionado por la librería jjwt-api para construir el token. Los claims incluyen los roles del usuario, lo que permite incorporar los scopes necesarios para la autorización en solicitudes posteriores.

Es esencial manejar la clave secreta de forma segura. La clave se puede definir utilizando el estándar SecretKey de Java:

@Configuration
public class JwtConfig {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Bean
    public SecretKey secretKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
    }
}

La clave secreta se configura en el archivo application.properties:

jwt.secret=TuClaveSecretaDeAdecuadaLongitud

Asegúrese de que la clave secreta tenga la longitud adecuada para el algoritmo de firma elegido, en este caso HS256.

Para que el AuthenticationManager pueda autenticar al usuario, se debe configurar un UserDetailsService personalizado que cargue los detalles del usuario desde la base de datos:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
    }
}

Creación de filtro JWT para verificación de token de cabecera Authorization

Para asegurar que solo los usuarios autenticados puedan acceder a los recursos protegidos de nuestra API REST, es necesario implementar un filtro JWT que verifique el token proporcionado en la cabecera Authorization de las peticiones. Este filtro se encargará de validar el token, extraer la información de autenticación y establecerla en el contexto de seguridad de Spring Security.

En primer lugar, crearemos una clase que extienda de OncePerRequestFilter, lo que garantiza que el filtro se ejecute una sola vez por petición. El filtro interceptará cada solicitud entrante y verificará la presencia y validez del token JWT.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String jwtToken = authorizationHeader.substring(7);

            String username = jwtService.extractUsername(jwtToken);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                if (jwtService.isTokenValid(jwtToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(
                                    userDetails, null, userDetails.getAuthorities());

                    authenticationToken.setDetails(
                            new WebAuthenticationDetailsSource().buildDetails(request));

                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }

        filterChain.doFilter(request, response);
    }
}

En este filtro, primero verificamos si la cabecera Authorization está presente y comienza con el prefijo "Bearer ". Luego, extraemos el token JWT y utilizamos el JwtService para obtener el nombre de usuario contenido en el token. Si el usuario no está autenticado en el contexto de seguridad, cargamos los detalles del usuario y validamos el token.

La validación del token se realiza mediante el método isTokenValid del JwtService, que comprueba la firma y la expiración del token. Si el token es válido, creamos un objeto UsernamePasswordAuthenticationToken y lo establecemos en el contexto de seguridad. Esto permite que Spring Security conozca al usuario autenticado y sus authorities para la autorización de futuras peticiones.

El JwtService debe implementar los métodos utilizados en el filtro:

@Service
public class JwtService {

    private final SecretKey secretKey;

    public JwtService(SecretKey secretKey) {
        this.secretKey = secretKey;
    }

    public String extractUsername(String token) {
        return Jwts.parser()
                    .verifyWith(secretKey)
                   .build()
                   .parseSignedClaims(token)
                   .getPayload()
                   .getSubject();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        Date expiration = Jwts.parser()
                              .verifyWith(secretKey)
                              .build()
                              .parseSignedClaims(token)
                              .getPayload()
                              .getExpiration();
        return expiration.before(new Date());
    }
}

Con estos métodos, el JwtService es capaz de extraer información del token y verificar su validez. Es crucial que la clave secreta utilizada para firmar y verificar el token sea la misma que en la generación del token durante el proceso de login.

CUIDADO: Es probable que estos métodos de la librería jjwt cambien, por lo que es mejor revisar su documentación oficial.

A continuación, configuraremos el SecurityFilterChain para incluir nuestro filtro JWT en la cadena de filtros de Spring Security. Esto se realiza en la clase de configuración de seguridad:

@Configuration
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/api/auth/**").permitAll()
                        .anyRequest().authenticated())
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

En esta configuración, deshabilitamos CSRF porque nuestra API REST es sin estado (stateless) y no utiliza formularios. Especificamos que las solicitudes a "/api/auth/**" están permitidas sin autenticar (por ejemplo, para el login y registro), y que cualquier otra solicitud requiere autenticación.

Utilizamos sessionManagement para indicar que no se debe crear sesión de usuario en el servidor, ya que la autenticación se maneja mediante tokens JWT.

Finalmente, agregamos nuestro filtro JWT antes del filtro de autenticación por defecto de Spring Security mediante el método addFilterBefore. De esta manera, el JwtAuthenticationFilter interceptará las peticiones y realizará la autenticación basada en el token antes de que se apliquen otros filtros.

Es importante recordar que el JwtAuthenticationFilter debe estar registrado como un @Component o ser definido como un @Bean para que Spring pueda inyectarlo y gestionarlo adecuadamente.

Con esta configuración, cualquier petición que incluya un token JWT válido en la cabecera Authorization podrá ser autenticada y el usuario será reconocido por Spring Security. Esto permite aplicar políticas de autorización basadas en los roles y permisos del usuario autenticado.

Además, es recomendable manejar las excepciones que puedan ocurrir durante la verificación del token, para proporcionar respuestas apropiadas al cliente en caso de errores. Esto se puede lograr extendiendo el filtro y capturando las excepciones específicas de JWT:

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain) throws ServletException, IOException {
    try {
        // Código de verificación del token
    } catch (JwtException e) {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write("Token JWT inválido: " + e.getMessage());
        return;
    }

    filterChain.doFilter(request, response);
}

Esta práctica mejora la seguridad y la experiencia del usuario al proporcionar mensajes claros y estados HTTP correctos.

Al crear un filtro JWT personalizado y configurarlo en el SecurityFilterChain, logramos que Spring Security autentique y autorice las solicitudes basándose en los tokens JWT proporcionados por los clientes. Esto es esencial para mantener una API REST segura y conforme a las mejores prácticas actuales.

Protección de rutas en SecurityFilterChain

Para garantizar una adecuada autorización en nuestra API REST, es esencial configurar la protección de rutas en el SecurityFilterChain utilizando los roles definidos en nuestra aplicación. Esto permite controlar el acceso a los diferentes endpoints según los privilegios del usuario autenticado.

En la configuración de seguridad, podemos definir reglas que especifiquen qué roles tienen permiso para acceder a cada ruta. En Spring Security, esto se logra mediante el método authorizeHttpRequests en el HttpSecurity:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/api/auth/**").permitAll()
                    .requestMatchers(HttpMethod.GET, "/api/products/**")
                        .hasAnyRole("USER", "ADMIN")
                    .requestMatchers("/api/products/**")
                        .hasRole("ADMIN")
                    .anyRequest().authenticated())
            .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
}

En este código, las rutas que coinciden con /api/auth/** están permitidas sin autenticación. Las peticiones GET a /api/products/** requieren que el usuario tenga el rol USER o ADMIN, mientras que las demás peticiones a /api/products/** (como POST, PUT o DELETE) solo están permitidas para usuarios con el rol ADMIN.

Es importante tener en cuenta que Spring Security espera que los roles tengan el prefijo ROLE_. Si los roles en nuestra base de datos no incluyen este prefijo, debemos ajustar la manera en que se convierten a authorities:

@Entity
@Table(name = "users")
public class User implements UserDetails {

    // Otros campos y métodos

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .toList();
    }

    // Implementaciones de otros métodos de UserDetails
}

Al mapear los roles del usuario con el prefijo ROLE_, garantizamos que las reglas de autorización funcionen correctamente según las convenciones de Spring Security.

Para asignar el usuario autenticado al crear un nuevo producto, podemos obtener el usuario actual desde el contexto de seguridad en el controlador. Esto se logra accediendo al SecurityContextHolder y extrayendo la información de autenticación:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;
    private final UserRepository userRepository;

    public ProductController(ProductService productService, UserRepository userRepository) {
        this.productService = productService;
        this.userRepository = userRepository;
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody @Valid Product product) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        User currentUser = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));

        product.setAuthor(currentUser);
        Product savedProduct = productService.saveProduct(product);

        return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
    }

    // Otros métodos
}

En este fragmento, obtenemos el nombre de usuario del usuario autenticado y lo utilizamos para recuperar la entidad User de la base de datos. Luego, asignamos este usuario como autor del producto antes de guardarlo.

Para mejorar la reutilización y limpieza del código, es conveniente encapsular la lógica de obtención del usuario autenticado en un componente dedicado:

@Component
public class AuthenticationFacade {

    private final UserRepository userRepository;

    public AuthenticationFacade(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getAuthenticatedUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();
        return userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
    }
}

Con esta facade, simplificamos el controlador de productos:

@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody @Valid Product product) {
    User currentUser = authenticationFacade.getAuthenticatedUser();
    product.setAuthor(currentUser);
    Product savedProduct = productService.saveProduct(product);

    return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
}

Al aplicar restricciones de acceso basadas en roles, garantizamos que solo los usuarios autorizados puedan realizar ciertas operaciones. Por ejemplo, podemos asegurar que solo los usuarios con rol ADMIN puedan crear nuevos productos:

.requestMatchers(HttpMethod.POST, "/api/products/**")
    .hasRole("ADMIN")

Es esencial que los roles estén correctamente definidos y asignados a los usuarios. Al autenticar al usuario, Spring Security utiliza las authorities proporcionadas por el UserDetails para evaluar las reglas de autorización.

Adicionalmente, para manejar escenarios donde un usuario necesita múltiples roles, nuestra entidad User ya está configurada con una relación ManyToMany con Role. Esto facilita la asignación de varios roles a un usuario y la gestión dinámica de permisos.

Al crear el producto, la entidad quedará asociada al usuario autenticado gracias a la asignación realizada en el controlador. Esto es crucial para mantener un historial de acciones y para aplicar reglas de negocio que dependan del autor del producto.

Es importante realizar pruebas exhaustivas de las reglas de autorización y de la asignación del usuario autenticado. Utilizar herramientas como Postman o cURL para enviar solicitudes con diferentes tokens JWT y verificar que se cumplen las restricciones establecidas es una práctica recomendada.

Además, considerar el manejo de excepciones y la personalización de los mensajes de error puede mejorar la experiencia de usuario y facilitar la identificación de problemas de acceso.

Aprende SpringBoot GRATIS online

Ejercicios de esta lección Seguridad JWT en API REST Spring Web

Evalúa tus conocimientos de esta lección Seguridad JWT en API REST Spring Web 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

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