2023-09-12 00:04:10 -03:00
|
|
|
# API Rest Java - Buenas prácticas y protección
|
|
|
|
|
|
|
|
Continuación de [Desarrollo de una API Rest](./spring_boot_1.md) donde se vio:
|
|
|
|
|
|
|
|
- Creación de un API Rest
|
|
|
|
- Crud (Create, Read, Update, Delete)
|
|
|
|
- Validaciones
|
|
|
|
- Paginación y orden
|
|
|
|
|
|
|
|
### Objetivos
|
|
|
|
|
|
|
|
- Buenas prácticas al desarrollar un API
|
|
|
|
- Tratamiento de errores
|
|
|
|
- Autenticación y Autorización
|
|
|
|
- Tokens JWT
|
|
|
|
|
|
|
|
## Buenas prácticas
|
|
|
|
|
|
|
|
Se modifican las respuestas de la API
|
|
|
|
|
|
|
|
[DatosRespuestaMedico](./api_rest/api2/src/main/java/med/voll/api/medico/DatosRespuestaMedico.java)
|
|
|
|
|
|
|
|
```java
|
|
|
|
public record DatosRespuestaMedico(
|
|
|
|
@NotNull Long id, String nombre,
|
|
|
|
String email, String telefono, String documento,
|
|
|
|
DatosDireccion direccion) {}
|
|
|
|
```
|
|
|
|
|
|
|
|
[MedicoController](./api_rest/api2/src/main/java/med/voll/api/controller/MedicoController.java)
|
|
|
|
|
|
|
|
```java
|
|
|
|
@RestController
|
|
|
|
@RequestMapping("/medicos")
|
|
|
|
public class MedicoController {
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
private MedicoRepository medicoRepository;
|
|
|
|
|
|
|
|
@PostMapping
|
|
|
|
public ResponseEntity<DatosRespuestaMedico> registrarMedico(
|
|
|
|
@RequestBody @Valid DatosRegistroMedico datosRegistroMedico,
|
|
|
|
UriComponentsBuilder uriComponentsBuilder) {
|
|
|
|
Medico medico = medicoRepository.save(new Medico(datosRegistroMedico));
|
|
|
|
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
|
|
|
|
medico.getId(),
|
|
|
|
medico.getNombre(),
|
|
|
|
medico.getEmail(),
|
|
|
|
medico.getTelefono(),
|
|
|
|
medico.getDocumento(),
|
|
|
|
new DatosDireccion(
|
|
|
|
medico.getDireccion().getCalle(),
|
|
|
|
medico.getDireccion().getDistrito(),
|
|
|
|
medico.getDireccion().getCiudad(),
|
|
|
|
medico.getDireccion().getNumero(),
|
|
|
|
medico.getDireccion().getComplemento()
|
|
|
|
)
|
|
|
|
);
|
|
|
|
URI url = uriComponentsBuilder.path("/medicos/{id}")
|
|
|
|
.buildAndExpand(medico.getId()).toUri();
|
|
|
|
return ResponseEntity.created(url).body(datosRespuestaMedico);
|
|
|
|
}
|
|
|
|
|
|
|
|
@GetMapping
|
|
|
|
public Page<DatosListadoMedicos> listadoMedicos(@PageableDefault(size = 5)
|
|
|
|
Pageable paginacion) {
|
|
|
|
return medicoRepository.findByActivoTrue(paginacion)
|
|
|
|
.map(DatosListadoMedicos::new);
|
|
|
|
}
|
|
|
|
|
|
|
|
@PutMapping
|
|
|
|
@Transactional
|
|
|
|
public ResponseEntity actualizarMedico(@RequestBody @Valid DatosActualizarMedico
|
|
|
|
datosActualizarMedico) {
|
|
|
|
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
|
|
|
|
medico.actualizarDatos(datosActualizarMedico);
|
|
|
|
return ResponseEntity.ok(
|
|
|
|
new DatosRespuestaMedico(
|
|
|
|
medico.getId(),
|
|
|
|
medico.getNombre(),
|
|
|
|
medico.getEmail(),
|
|
|
|
medico.getTelefono(),
|
|
|
|
medico.getDocumento(),
|
|
|
|
new DatosDireccion(
|
|
|
|
medico.getDireccion().getCalle(),
|
|
|
|
medico.getDireccion().getDistrito(),
|
|
|
|
medico.getDireccion().getCiudad(),
|
|
|
|
medico.getDireccion().getNumero(),
|
|
|
|
medico.getDireccion().getComplemento()
|
|
|
|
)
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
@DeleteMapping("/{id}")
|
|
|
|
@Transactional
|
|
|
|
public ResponseEntity eliminarMedico(@PathVariable Long id) {
|
|
|
|
Medico medico = medicoRepository.getReferenceById(id);
|
|
|
|
medico.desactivarMedico();
|
|
|
|
return ResponseEntity.noContent().build();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
## Codigos de respuesta del protocolo HTTP
|
|
|
|
|
|
|
|
El protocolo HTTP (***Hypertext Transfer Protocol***, *RFC 2616*) es el protocolo
|
|
|
|
encargado de realizar la comunicación entre el cliente, que suele ser un
|
|
|
|
navegador, y el servidor. De esta forma, para cada *solicitud* realizada por el
|
|
|
|
cliente, el servidor responde si tuvo éxito o no. Si no tiene éxito, la mayoría
|
|
|
|
de las veces, la respuesta del servidor será una secuencia numérica acompañada
|
|
|
|
de un mensaje.
|
|
|
|
|
|
|
|
Categoría de código
|
|
|
|
Los códigos **HTTP** (o **HTTPS**) tienen tres dígitos, y el primer dígito
|
|
|
|
representa la clasificación dentro de las cinco categorías posibles.
|
|
|
|
|
|
|
|
- **`1XX` Informativo:** la solicitud fue aceptada o el proceso aún está en curso
|
|
|
|
- **`2XX` Confirmación:** la acción se completó o se comprendió
|
|
|
|
- **`3XX` Redirección:** indica que se debe hacer o se debió hacer algo más para
|
|
|
|
completar la solicitud
|
|
|
|
- **`4XX` Error del cliente:** indica que la solicitud no se puede completar o
|
|
|
|
contiene una sintaxis incorrecta
|
|
|
|
- **`5XX` Error del servidor:** el servidor falló al concluir la solicitud.
|
|
|
|
|
|
|
|
### Principales códigos de error
|
|
|
|
|
|
|
|
Estos permiten comprender mejor la comunicación de su navegador con el servidor
|
|
|
|
de la aplicación que se intenta acceder.
|
|
|
|
|
|
|
|
#### Error 403
|
|
|
|
|
|
|
|
El código 403 es el error **"Prohibido"**. Significa que el servidor entendió
|
|
|
|
la solicitud del cliente, pero se niega a procesarla, ya que el cliente no está
|
|
|
|
autorizado para hacerlo.
|
|
|
|
|
|
|
|
#### Error 404
|
|
|
|
|
|
|
|
Mensaje de Error 404, significa que la URL no lo llevó a ninguna parte.
|
|
|
|
Puede ser que la aplicación ya no exista, que la URL haya cambiado o que haya
|
|
|
|
ingresado una URL incorrecta.
|
|
|
|
|
|
|
|
#### Error 500
|
|
|
|
|
|
|
|
Es un error menos común, pero aparece de vez en cuando. Este error significa que
|
|
|
|
hay un problema con una de las bases que hace que se ejecute una aplicación.
|
|
|
|
Básicamente, este error puede estar en el servidor que mantiene la aplicación
|
|
|
|
en línea o en la comunicación con el sistema de archivos, que proporciona la
|
|
|
|
infraestructura para la aplicación.
|
|
|
|
|
|
|
|
#### Error 503
|
|
|
|
|
|
|
|
El error 503 significa que el servicio al que se accede no está disponible
|
|
|
|
temporalmente. Las causas comunes son un servidor que está fuera de servicio
|
|
|
|
por mantenimiento o sobrecargado. Los ataques maliciosos como ***DDoS***
|
|
|
|
causan mucho este problema.
|
|
|
|
|
|
|
|
|
|
|
|
Para consultar sobre algún código HTTP, se puede usar la sgte. página:
|
|
|
|
|
|
|
|
[http cat](https://http.cat)
|
|
|
|
|
|
|
|
ejm consulta por código `405 Method Not Allowed`
|
|
|
|
|
|
|
|
```http
|
|
|
|
https://http.cat/405
|
|
|
|
```
|
|
|
|
|
2023-09-12 14:48:40 -03:00
|
|
|
## Manejando errores
|
|
|
|
|
|
|
|
- Spring boot common
|
|
|
|
[properties](https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html)
|
|
|
|
|
|
|
|
- Spring boot server
|
|
|
|
[properties](https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.server)
|
|
|
|
|
|
|
|
#### Ocultando el stacktrace
|
|
|
|
|
|
|
|
[application.properties](./api_rest/api2/src/main/resources/application.properties)
|
|
|
|
|
|
|
|
```conf
|
|
|
|
server.error.include-stacktrace=never
|
|
|
|
```
|
|
|
|
|
|
|
|
Nuevo package [infra](./api_rest/api2/src/main/java/med/voll/api/infra) con nueva
|
|
|
|
clase
|
|
|
|
[ManejadorDeErrores](./api_rest/api2/src/main/java/med/voll/api/infra/ManejadorDeErrores.java)
|
|
|
|
|
|
|
|
```java
|
|
|
|
...
|
|
|
|
|
|
|
|
@RestControllerAdvice
|
|
|
|
public class ManejadorDeErrores {
|
|
|
|
|
|
|
|
@ExceptionHandler(EntityNotFoundException.class)
|
|
|
|
public ResponseEntity manejarError404(){
|
|
|
|
return ResponseEntity.notFound().build();
|
|
|
|
}
|
|
|
|
|
|
|
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
|
|
public ResponseEntity manejarError400(MethodArgumentNotValidException e){
|
|
|
|
var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList();
|
|
|
|
return ResponseEntity.badRequest().body(errores);
|
|
|
|
}
|
|
|
|
|
|
|
|
private record DatosErrorValidacion(String campo, String error) {
|
|
|
|
public DatosErrorValidacion(FieldError error) {
|
|
|
|
this(error.getField(), error.getDefaultMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Por defecto, **Bean Validation** devuelve mensajes de error en inglés, sin
|
|
|
|
embargo, hay una traducción de estos mensajes al español ya implementada en
|
|
|
|
esta especificación.
|
|
|
|
|
|
|
|
En el protocolo HTTP hay un encabezado llamado `Accept-Language`, que sirve para
|
|
|
|
indicar al servidor el idioma preferido del cliente que activa la solicitud.
|
|
|
|
Podemos utilizar esta cabecera para indicarle a Spring el idioma deseado, para
|
|
|
|
que en la integración con Bean Validation pueda buscar mensajes según el idioma
|
|
|
|
indicado.
|
|
|
|
|
|
|
|
En Insomnia, y también en otras herramientas similares, existe una opción
|
|
|
|
llamada Header en la que podemos incluir cabeceras a enviar en la petición.
|
|
|
|
Si agregamos el encabezado `Accept-Language` con el valor `es`, los mensajes de
|
|
|
|
error de **Bean Validation** se devolverán automáticamente en español.
|
|
|
|
|
|
|
|
> Nota: Bean Validation solo traduce los mensajes de error a unos pocos idiomas.
|
|
|
|
|
|
|
|
### Personalización de mensajes de error
|
|
|
|
|
|
|
|
**Bean Validation** tiene un mensaje de error para cada una de sus anotaciones.
|
|
|
|
P.e. cuando la validación falla en algún atributo anotado con `@NotBlank`, el
|
|
|
|
mensaje de error será: `must not be blank`.
|
|
|
|
|
|
|
|
Estos mensajes de error no se definieron en la aplicación, ya que son mensajes
|
|
|
|
de error estándar de Bean Validation. Sin embargo, si lo desea, puede
|
|
|
|
personalizar dichos mensajes.
|
|
|
|
|
|
|
|
Una de las formas de personalizar los mensajes de error es agregar el atributo
|
|
|
|
del mensaje a las anotaciones de validación:
|
|
|
|
|
|
|
|
```java
|
|
|
|
public record DatosCadastroMedico(
|
|
|
|
@NotBlank(message = "Nombre es obligatorio")
|
|
|
|
String nombre,
|
|
|
|
|
|
|
|
@NotBlank(message = "Email es obligatorio")
|
|
|
|
@Email(message = "Formato de email es inválido")
|
|
|
|
String email,
|
|
|
|
|
|
|
|
@NotBlank(message = "Teléfono es obligatorio")
|
|
|
|
String telefono,
|
|
|
|
|
|
|
|
@NotBlank(message = "CRM es obligatorio")
|
|
|
|
@Pattern(regexp = "\\d{4,6}", message = "Formato do CRM es inválido")
|
|
|
|
String crm,
|
|
|
|
|
|
|
|
@NotNull(message = "Especialidad es obligatorio")
|
|
|
|
Especialidad especialidad,
|
|
|
|
|
|
|
|
@NotNull(message = "Datos de dirección son obligatorios")
|
|
|
|
@Valid DatosDireccion direccion) {}
|
|
|
|
```
|
|
|
|
|
|
|
|
Otra forma es aislar los mensajes en un archivo de propiedades, que debe tener
|
|
|
|
el nombre `ValidationMessages.properties` y estar creado en el directorio
|
|
|
|
`src/main/resources`:
|
|
|
|
|
|
|
|
```config
|
|
|
|
nombre.obligatorio=El nombre es obligatorio
|
|
|
|
email.obligatorio=Correo electrónico requerido
|
|
|
|
email.invalido=El formato del correo electrónico no es válido
|
|
|
|
phone.obligatorio=Teléfono requerido
|
|
|
|
crm.obligatorio=CRM es obligatorio
|
|
|
|
crm.invalido=El formato CRM no es válido
|
|
|
|
especialidad.obligatorio=La especialidad es obligatoria
|
|
|
|
address.obligatorio=Los datos de dirección son obligatorios
|
|
|
|
```
|
|
|
|
|
|
|
|
Y, en las anotaciones, indicar la clave de las propiedades por el propio atributo
|
|
|
|
message, delimitando con los caracteres `{` y `}`:
|
|
|
|
|
|
|
|
```java
|
|
|
|
public record DatosRegistroMedico(
|
|
|
|
@NotBlank(message = "{nombre.obligatorio}")
|
|
|
|
String nombre,
|
|
|
|
|
|
|
|
@NotBlank(message = "{email.obligatorio}")
|
|
|
|
@Email(message = "{email.invalido}")
|
|
|
|
String email,
|
|
|
|
|
|
|
|
@NotBlank(message = "{telefono.obligatorio}")
|
|
|
|
String telefono,
|
|
|
|
|
|
|
|
@NotBlank(message = "{crm.obligatorio}")
|
|
|
|
@Pattern(regexp = "\\d{4,6}", message = "{crm.invalido}")
|
|
|
|
String crm,
|
|
|
|
|
|
|
|
@NotNull(message = "{especialidad.obligatorio}")
|
|
|
|
Especialidad especialidad,
|
|
|
|
|
|
|
|
@NotNull(message = "{direccion.obligatorio}")
|
|
|
|
@Valid DatosDireccion direccion) {}
|
|
|
|
```
|
|
|
|
|