Registro de usuarios en API REST

Avanzado
Spring Security
Spring Security
Actualizado: 04/05/2026

Diagrama: tutorial-spring-registro-usuarios

En esta lección configuramos un método register en un controlador REST que permita dar de alta nuevos empleados en el portal interno. Se presupone una aplicación Spring Boot con base de datos PostgreSQL y una entidad Empleado ya modelada.

Agregar dependencia Spring Security

En el archivo pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Clase SecurityConfig y PasswordEncoder

Configuramos la seguridad a nivel global con el patrón moderno basado en SecurityFilterChain (nunca WebSecurityConfigurerAdapter, deprecated desde Spring Security 5.7 y eliminado en 6.x).

Para el PasswordEncoder hay dos opciones válidas en 2026:

  • Argon2id (recomendado por OWASP para aplicaciones nuevas): resistente a ataques con GPU.
  • BCrypt con strength = 12: extendido, maduro, aceptable.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Valores OWASP 2026: 19 MB de memoria, 2 iteraciones, 1 hilo.
        return new Argon2PasswordEncoder(16, 32, 1, 19456, 2);
    }
}

Si prefieres BCrypt por compatibilidad con hashes existentes en otros sistemas:

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

Para soportar ambos algoritmos a la vez durante una migración, ver el tutorial específico de DelegatingPasswordEncoder en el módulo de fundamentos.

Crear método register en controlador

Primero el record RegistroEmpleadoRequest con validación Bean Validation:

public record RegistroEmpleadoRequest(
        @NotBlank @Email String email,
        @NotBlank @Size(min = 12, message = "La contraseña debe tener al menos 12 caracteres") String password,
        @NotBlank String nombre,
        @NotBlank String departamento
) {
}

Definimos una excepción de dominio en lugar de RuntimeException genérica. Esto permite mapear correctamente el código HTTP y evita filtrar detalles internos:

public class EmailYaRegistradoException extends RuntimeException {
    public EmailYaRegistradoException(String email) {
        super("El email " + email + " ya está registrado");
    }
}

Ahora el método en el controlador, delegando la lógica en un servicio:

@RestController
@RequestMapping("/api/empleados")
public class EmpleadoController {

    private final EmpleadoService empleadoService;

    public EmpleadoController(EmpleadoService empleadoService) {
        this.empleadoService = empleadoService;
    }

    @PostMapping("/register")
    @ResponseStatus(HttpStatus.CREATED)
    public EmpleadoResumenDto register(@RequestBody @Valid RegistroEmpleadoRequest request) {
        return empleadoService.registrar(request);
    }
}

El servicio encapsula la lógica, hace el cifrado y rechaza duplicados:

@Service
public class EmpleadoService {

    private final EmpleadoRepository empleadoRepository;
    private final PasswordEncoder passwordEncoder;

    public EmpleadoService(EmpleadoRepository empleadoRepository,
                           PasswordEncoder passwordEncoder) {
        this.empleadoRepository = empleadoRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Transactional
    public EmpleadoResumenDto registrar(RegistroEmpleadoRequest request) {
        String emailNormalizado = request.email().toLowerCase().trim();

        if (empleadoRepository.existsByEmail(emailNormalizado)) {
            throw new EmailYaRegistradoException(emailNormalizado);
        }

        Empleado empleado = Empleado.builder()
                .email(emailNormalizado)
                .password(passwordEncoder.encode(request.password()))
                .nombre(request.nombre())
                .departamento(request.departamento())
                .rol(Rol.EMPLEADO)
                .activo(true)
                .fechaAlta(Instant.now())
                .build();

        Empleado guardado = empleadoRepository.save(empleado);
        return new EmpleadoResumenDto(guardado.getId(), guardado.getEmail(), guardado.getNombre());
    }
}

Mapear la excepción a HTTP 409 Conflict

@RestControllerAdvice
public class EmpleadoExceptionHandler {

    @ExceptionHandler(EmailYaRegistradoException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Map<String, String> emailDuplicado(EmailYaRegistradoException ex) {
        return Map.of("error", "email_ocupado", "mensaje", ex.getMessage());
    }
}

Con este patrón el controlador queda limpio, el servicio tiene la regla de negocio y el @RestControllerAdvice centraliza el mapeo HTTP. Nada de throw new RuntimeException(...) en el flujo principal.

Verificar creación del empleado

Desde Postman enviamos un POST /api/empleados/register con el JSON del nuevo empleado y verificamos que la respuesta sea 201 Created con el DTO de resumen.

En la base de datos comprobamos que la contraseña aparece cifrada (arranca con $argon2id$ o $2a$ según el encoder):

Un segundo POST con el mismo email debe devolver 409 Conflict con el JSON {"error": "email_ocupado", ...}.

También es posible realizar el registro desde un frontend Angular invocando HttpClient.post('/api/empleados/register', ...):

Para reforzar el endpoint en producción conviene añadir rate limiting por IP (ver la lección de Bucket4j) y una política de contraseñas basada en listas de contraseñas filtradas (por ejemplo, integrando HaveIBeenPwned con un Pwned Passwords check antes de aceptar la alta).

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Spring Security

Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Crear cuentas de empleados con contraseña cifrada. Elegir entre BCrypt y Argon2id como PasswordEncoder. Validar email corporativo único. Diferenciar excepciones de dominio del mapeo HTTP con RestControllerAdvice.