Spring Boot 3 - Buenas prácticas y protección: Aula 3

This commit is contained in:
devfzn 2023-09-13 14:51:06 -03:00
parent e9fbc012c2
commit d2d56e2a91
Signed by: devfzn
GPG Key ID: E070ECF4A754FDB1
15 changed files with 515 additions and 50 deletions

View File

@ -21,7 +21,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
@ -63,6 +62,15 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,35 @@
package med.voll.api.controller;
import jakarta.validation.Valid;
import med.voll.api.domain.usuario.DatosAutenticacionUsuario;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/login")
public class AutenticacionController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping
public ResponseEntity autenticarUsuario(@RequestBody @Valid DatosAutenticacionUsuario datosAutenticacionUsuario) {
Authentication token = new UsernamePasswordAuthenticationToken(
datosAutenticacionUsuario.login(),
datosAutenticacionUsuario.clave());
authenticationManager.authenticate(token);
//var token = new UsernamePasswordAuthenticationToken(
// datosAutenticacionUsuario.login(),
// datosAutenticacionUsuario.clave());
//var autentication = authenticationManager.authenticate(token);
return ResponseEntity.ok().build();
}
}

View File

@ -29,21 +29,23 @@ public class MedicoController {
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()
)
medico, new DatosDireccion(medico.getDireccion())
);
//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);
// Debe retornar 201 Created
// Url donde encontrar al médico ej. http://127.0.0.1:8080/medicos/xx
}
@GetMapping
public ResponseEntity<Page<DatosListadoMedicos>> listadoMedicos(@PageableDefault(size = 5) Pageable paginacion) {
//return medicoRepository.findAll(paginacion).map(DatosListadoMedicos::new);
public ResponseEntity<Page<DatosListadoMedicos>> listadoMedicos(
@PageableDefault(size = 5) Pageable paginacion) {
return ResponseEntity.ok(medicoRepository.findByActivoTrue(paginacion).map(DatosListadoMedicos::new));
}
@ -53,16 +55,12 @@ public class MedicoController {
@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()
)
));
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
medico, new DatosDireccion(medico.getDireccion())
);
return ResponseEntity.ok(datosRespuestaMedico);
}
// Desactivar Medico
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity eliminarMedico(@PathVariable Long id) {
@ -74,14 +72,10 @@ public class MedicoController {
@GetMapping("/{id}")
public ResponseEntity<DatosRespuestaMedico> retornaDatosMedico(@PathVariable Long id) {
Medico medico = medicoRepository.getReferenceById(id);
var datosMedico = 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()
)
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
medico, new DatosDireccion(medico.getDireccion())
);
return ResponseEntity.ok(datosMedico);
return ResponseEntity.ok(datosRespuestaMedico);
}
}

View File

@ -29,35 +29,30 @@ public class PacienteController {
UriComponentsBuilder uriComponentsBuilder) {
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),
new DatosDireccion(
paciente.getDireccion().getCalle(), paciente.getDireccion().getDistrito(), paciente.getDireccion().getCiudad(),
paciente.getDireccion().getNumero(), paciente.getDireccion().getComplemento()
)
paciente, new DatosDireccion(paciente.getDireccion())
);
URI url = uriComponentsBuilder.path("/pacientes/{id}") .buildAndExpand(paciente.getId()).toUri();
return ResponseEntity.created(url).body(datosRespuestaPaciente);
}
@GetMapping
public ResponseEntity<Page<DatosListadoPacientes>> listadoPacientes(@PageableDefault(size = 5) Pageable paginacion) {
//return pacienteRepository.findAll(paginacion).map(DatosListadoPacientes::new);
return ResponseEntity.ok(pacienteRepository.findByActivoTrue(paginacion).map(DatosListadoPacientes::new));
public ResponseEntity<Page<DatosListadoPacientes>> listadoPacientes(
@PageableDefault(size = 5) Pageable paginacion) {
return ResponseEntity.ok(
pacienteRepository.findByActivoTrue(paginacion).map(DatosListadoPacientes::new)
);
}
@PutMapping
@Transactional
public ResponseEntity<DatosRespuestaPaciente> actualizarPaciente(@RequestBody @Valid DatosActualizarPaciente datosActualizarPaciente) {
public ResponseEntity<DatosRespuestaPaciente> actualizarPaciente(
@RequestBody @Valid DatosActualizarPaciente datosActualizarPaciente) {
Paciente paciente = pacienteRepository.getReferenceById(datosActualizarPaciente.id());
paciente.actualizarDatos(datosActualizarPaciente);
return ResponseEntity.ok(new DatosRespuestaPaciente(
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),
new DatosDireccion(
paciente.getDireccion().getCalle(), paciente.getDireccion().getDistrito(),
paciente.getDireccion().getCiudad(),paciente.getDireccion().getNumero(),
paciente.getDireccion().getComplemento()
)
));
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente, new DatosDireccion(paciente.getDireccion())
);
return ResponseEntity.ok(datosRespuestaPaciente);
}
// Desactivar Paciente
@ -72,14 +67,10 @@ public class PacienteController {
@GetMapping("/{id}")
public ResponseEntity<DatosRespuestaPaciente> retornaDatosPaciente(@PathVariable Long id) {
Paciente paciente = pacienteRepository.getReferenceById(id);
var datosPaciente = new DatosRespuestaPaciente(
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),
new DatosDireccion(
paciente.getDireccion().getCalle(), paciente.getDireccion().getDistrito(), paciente.getDireccion().getCiudad(),
paciente.getDireccion().getNumero(), paciente.getDireccion().getComplemento()
)
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
paciente, new DatosDireccion(paciente.getDireccion())
);
return ResponseEntity.ok(datosPaciente);
return ResponseEntity.ok(datosRespuestaPaciente);
}
}

View File

@ -8,4 +8,13 @@ public record DatosDireccion(
@NotBlank String ciudad,
String numero,
String complemento) {
public DatosDireccion(Direccion direccion){
this(direccion.getCalle(),
direccion.getDistrito(),
direccion.getCiudad(),
direccion.getNumero(),
direccion.getComplemento()
);
}
}

View File

@ -6,4 +6,14 @@ import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRespuestaMedico(@NotNull Long id, String nombre,
String email, String telefono, String documento,
DatosDireccion direccion) {
public DatosRespuestaMedico(Medico medico, DatosDireccion direccion){
this(medico.getId(),
medico.getNombre(),
medico.getEmail(),
medico.getTelefono(),
medico.getDocumento(),
direccion);
}
}

View File

@ -6,4 +6,14 @@ import med.voll.api.domain.direccion.DatosDireccion;
public record DatosRespuestaPaciente(@NotNull Long id, String nombre,
String email, String telefono, String documento,
DatosDireccion direccion) {
public DatosRespuestaPaciente(Paciente paciente, DatosDireccion direccion){
this(paciente.getId(),
paciente.getNombre(),
paciente.getEmail(),
paciente.getTelefono(),
paciente.getDocumento(),
direccion);
}
}

View File

@ -0,0 +1,5 @@
package med.voll.api.domain.usuario;
public record DatosAutenticacionUsuario(String login, String clave) {
}

View File

@ -0,0 +1,63 @@
package med.voll.api.domain.usuario;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Table(name = "usuarios")
@Entity(name = "Usuario")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Usuario implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private String clave;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return clave;
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@ -0,0 +1,9 @@
package med.voll.api.domain.usuario;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
UserDetails findByLogin(String login);
}

View File

@ -1,6 +1,7 @@
package med.voll.api.infra;
package med.voll.api.infra.errores;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
@ -21,6 +22,12 @@ public class ManejadorDeErrores {
return ResponseEntity.badRequest().body(errores);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity manejarError500(DataIntegrityViolationException e) {
var errores = e.getMostSpecificCause().getLocalizedMessage();
return ResponseEntity.badRequest().body(errores);
}
private record DatosErrorValidacion(String campo, String error) {
public DatosErrorValidacion(FieldError error) {
this(error.getField(), error.getDefaultMessage());

View File

@ -0,0 +1,20 @@
package med.voll.api.infra.security;
import med.voll.api.domain.usuario.UsuarioRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AutenticacionService implements UserDetailsService {
@Autowired
private UsuarioRepository usuarioRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
return usuarioRepository.findByLogin(login);
}
}

View File

@ -0,0 +1,35 @@
package med.voll.api.infra.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfigurations {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,9 @@
create table usuarios(
id bigint not null auto_increment,
login varchar(100) not null unique,
clave varchar(300) not null,
primary key(id)
);

View File

@ -304,3 +304,263 @@ public record DatosRegistroMedico(
@Valid DatosDireccion direccion) {}
```
### Seguridad
- Autenticación
- Autorización
- Protección contra ataques (CSRF, clickjacking)
```mermaid
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'cardinal'}}}%%
flowchart
direction TB
DB[(BBDD)]
subgraph "Autenticación"
subgraph APP["App o Web"]
direction LR
WEB("User
Pass")
end
subgraph REQ["HTTP Request"]
direction TB
DT{"user
pass"}
JWT{JWT}
end
subgraph API[API REST]
direction LR
Aplicación
end
APP --"user & pass"--> DT
JWT --"token"--> APP
REQ <--> API
API <--SQL--> DB
end
```
### Hash de contraseña
Al implementar una funcionalidad de autenticación en una aplicación,
independientemente del lenguaje de programación utilizado, deberá tratar con
los datos de inicio de sesión y contraseña de los usuarios, y deberán
almacenarse en algún lugar, como, por ejemplo, una base de datos.
Las contraseñas son información confidencial y no deben almacenarse en texto
sin formato, ya que si una persona malintencionada logra acceder a la base de
datos, podrá acceder a las contraseñas de todos los usuarios. Para evitar este
problema, siempre debe usar algún algoritmo hash en las contraseñas antes de
almacenarlas en la base de datos.
**Hashing** no es más que una función matemática que convierte un texto en otro
texto totalmente diferente y difícil de deducir. Por ejemplo, el texto *"Mi
nombre es Rodrigo"* se puede convertir en el texto
`8132f7cb860e9ce4c1d9062d2a5d1848`, utilizando el algoritmo ***hash MD5***.
Un detalle importante es que los algoritmos de hash deben ser unidireccionales,
es decir, no debe ser posible obtener el texto original a partir de un hash.
Así, para saber si un usuario ingresó la contraseña correcta al intentar
autenticarse en una aplicación, debemos tomar la contraseña que ingresó y
generar su hash, para luego compararla con el hash que está almacenado en la
base de datos.
Hay varios algoritmos hashing que se pueden usar para transformar las contraseñas
de los usuarios, algunos de los cuales son más antiguos y ya **no se consideran
seguros** en la actualidad, como **MD5** y **SHA1**. Los principales algoritmos
actualmente recomendados son:
- Bcrypt
- Scrypt
- Argon2
- PBKDF2
Se utilizará el algoritmo **BCrypt**, que es bastante popular hoy en día. Esta
opción también tiene en cuenta que ***Spring Security*** ya nos proporciona una
clase que lo implementa.
**Spring Data** usa su propio patrón de nomenclatura de métodos a seguir para
que pueda generar consultas SQL correctamente.
Hay algunas palabras reservadas que debemos usar en los nombres de los métodos,
como `findBy` y `existBy`, para indicarle a **Spring Data** cómo debe ensamblar
la consulta que queremos. Esta característica es bastante flexible y puede ser
un poco compleja debido a las diversas posibilidades existentes.
Para conocer más detalles y comprender mejor cómo ensamblar consultas dinámicas
con Spring Data, acceda a su
[documentación oficial](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/).
Bcrypt [online](https://www.browserling.com/tools/bcrypt)
#### Autenticación API
Se agregan las dependencias
[pom.xml](./api_rest/api2/pom.xml)
```xml
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
...
```
Creación de clases `Usuario`, `UsuarioRepository` y `DatosAutenticacionUsuario`
en *package*
[domain.usuario](./api_rest/api2/src/main/java/med/voll/api/domain/usuario/)
[Usuario](./api_rest/api2/src/main/java/med/voll/api/domain/usuario/Usuario.java)
```java
@Table(name = "usuarios")
@Entity(name = "Usuario")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Usuario implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String login;
private String clave;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() { return clave; }
@Override
public String getUsername() { return login; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
```
[UsuarioRepository](./api_rest/api2/src/main/java/med/voll/api/domain/usuario/UsuarioRepository.java)
```java
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
UserDetails findByLogin(String login);
}
```
[DatosAutenticacionUsuario](./api_rest/api2/src/main/java/med/voll/api/domain/usuario/DatosAutenticacionUsuario.java)
```java
public record DatosAutenticacionUsuario(String login, String clave) {}
```
Creación de clase
[AutenticacionController](./api_rest/api2/src/main/java/med/voll/api/controller/AutenticacionController.java)
en *package* [controller](./api_rest/api2/src/main/java/med/voll/api/controller/)
> En este punto no retorna *token*
```java
package med.voll.api.controller;
@RestController
@RequestMapping("/login")
public class AutenticacionController {
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping
public ResponseEntity autenticarUsuario(
@RequestBody @Valid DatosAutenticacionUsuario datosAutenticacionUsuario) {
Authentication token = new UsernamePasswordAuthenticationToken(
datosAutenticacionUsuario.login(),
datosAutenticacionUsuario.clave());
authenticationManager.authenticate(token);
return ResponseEntity.ok().build();
}
}
```
Creación de clases `AutenticationService` y `SecurityConfigurations` en *package*
[infra.security](./api_rest/api2/src/main/java/med/voll/api/infra/)
[AutenticationService](./api_rest/api2/src/main/java/med/voll/api/infra/security/AutenticacionService.java)
```java
package med.voll.api.infra.security;
@Service
public class AutenticacionService implements UserDetailsService {
@Autowired
private UsuarioRepository usuarioRepository;
@Override
public UserDetails loadUserByUsername(String login)
throws UsernameNotFoundException {
return usuarioRepository.findByLogin(login);
}
}
```
[SecurityConfigurations](./api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java)
```java
@Configuration
@EnableWebSecurity
public class SecurityConfigurations {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
throws Exception {
return httpSecurity.csrf().disable().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
}
```
Creación de nueva
[migración](./api_rest/api2/src/main/resources/db/migration/V5__create-table-usuarios.sql)
para crear tabla `usuarios`
```sql
create table usuarios(
id bigint not null auto_increment,
login varchar(100) not null unique,
clave varchar(300) not null,
primary key(id)
);
```