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í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 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.
- 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.
- 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
.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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);
}
}
- 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.
- 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.
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)
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
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