Compare commits
No commits in common. "dfd811fd81b02838b910c5f65e68b82db35fad11" and "e9fbc012c20f5f646ee40db75993b0421c8aec46" have entirely different histories.
dfd811fd81
...
e9fbc012c2
@ -21,6 +21,7 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
@ -62,21 +63,6 @@
|
|||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</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>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.auth0</groupId>
|
|
||||||
<artifactId>java-jwt</artifactId>
|
|
||||||
<version>4.4.0</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
package med.voll.api.controller;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import med.voll.api.domain.usuario.DatosAutenticacionUsuario;
|
|
||||||
import med.voll.api.domain.usuario.Usuario;
|
|
||||||
import med.voll.api.infra.security.DatosJWTtoken;
|
|
||||||
import med.voll.api.infra.security.TokenService;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private TokenService tokenService;
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity autenticarUsuario(@RequestBody @Valid DatosAutenticacionUsuario datosAutenticacionUsuario) {
|
|
||||||
Authentication authtoken = new UsernamePasswordAuthenticationToken(
|
|
||||||
datosAutenticacionUsuario.login(),
|
|
||||||
datosAutenticacionUsuario.clave());
|
|
||||||
var usuarioAutenticado = authenticationManager.authenticate(authtoken);
|
|
||||||
var JWTtoken = tokenService.generarToken((Usuario) usuarioAutenticado.getPrincipal());
|
|
||||||
return ResponseEntity.ok(new DatosJWTtoken(JWTtoken));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -29,23 +29,21 @@ public class MedicoController {
|
|||||||
UriComponentsBuilder uriComponentsBuilder) {
|
UriComponentsBuilder uriComponentsBuilder) {
|
||||||
Medico medico = medicoRepository.save(new Medico(datosRegistroMedico));
|
Medico medico = medicoRepository.save(new Medico(datosRegistroMedico));
|
||||||
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
|
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
|
||||||
medico, new DatosDireccion(medico.getDireccion())
|
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.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();
|
URI url = uriComponentsBuilder.path("/medicos/{id}") .buildAndExpand(medico.getId()).toUri();
|
||||||
return ResponseEntity.created(url).body(datosRespuestaMedico);
|
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
|
@GetMapping
|
||||||
public ResponseEntity<Page<DatosListadoMedicos>> listadoMedicos(
|
public ResponseEntity<Page<DatosListadoMedicos>> listadoMedicos(@PageableDefault(size = 5) Pageable paginacion) {
|
||||||
@PageableDefault(size = 5) Pageable paginacion) {
|
//return medicoRepository.findAll(paginacion).map(DatosListadoMedicos::new);
|
||||||
return ResponseEntity.ok(medicoRepository.findByActivoTrue(paginacion).map(DatosListadoMedicos::new));
|
return ResponseEntity.ok(medicoRepository.findByActivoTrue(paginacion).map(DatosListadoMedicos::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,12 +53,16 @@ public class MedicoController {
|
|||||||
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
|
@RequestBody @Valid DatosActualizarMedico datosActualizarMedico) {
|
||||||
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
|
Medico medico = medicoRepository.getReferenceById(datosActualizarMedico.id());
|
||||||
medico.actualizarDatos(datosActualizarMedico);
|
medico.actualizarDatos(datosActualizarMedico);
|
||||||
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
|
return ResponseEntity.ok(new DatosRespuestaMedico(
|
||||||
medico, new DatosDireccion(medico.getDireccion())
|
medico.getId(), medico.getNombre(), medico.getEmail(), medico.getTelefono(), medico.getDocumento(),
|
||||||
);
|
new DatosDireccion(
|
||||||
return ResponseEntity.ok(datosRespuestaMedico);
|
medico.getDireccion().getCalle(), medico.getDireccion().getDistrito(), medico.getDireccion().getCiudad(),
|
||||||
|
medico.getDireccion().getNumero(), medico.getDireccion().getComplemento()
|
||||||
|
)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Desactivar Medico
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity eliminarMedico(@PathVariable Long id) {
|
public ResponseEntity eliminarMedico(@PathVariable Long id) {
|
||||||
@ -72,10 +74,14 @@ public class MedicoController {
|
|||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<DatosRespuestaMedico> retornaDatosMedico(@PathVariable Long id) {
|
public ResponseEntity<DatosRespuestaMedico> retornaDatosMedico(@PathVariable Long id) {
|
||||||
Medico medico = medicoRepository.getReferenceById(id);
|
Medico medico = medicoRepository.getReferenceById(id);
|
||||||
DatosRespuestaMedico datosRespuestaMedico = new DatosRespuestaMedico(
|
var datosMedico = new DatosRespuestaMedico(
|
||||||
medico, new DatosDireccion(medico.getDireccion())
|
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()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok(datosRespuestaMedico);
|
return ResponseEntity.ok(datosMedico);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,30 +29,35 @@ public class PacienteController {
|
|||||||
UriComponentsBuilder uriComponentsBuilder) {
|
UriComponentsBuilder uriComponentsBuilder) {
|
||||||
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
|
Paciente paciente = pacienteRepository.save(new Paciente(datosRegistroPaciente));
|
||||||
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
|
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
|
||||||
paciente, new DatosDireccion(paciente.getDireccion())
|
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()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
URI url = uriComponentsBuilder.path("/pacientes/{id}") .buildAndExpand(paciente.getId()).toUri();
|
URI url = uriComponentsBuilder.path("/pacientes/{id}") .buildAndExpand(paciente.getId()).toUri();
|
||||||
return ResponseEntity.created(url).body(datosRespuestaPaciente);
|
return ResponseEntity.created(url).body(datosRespuestaPaciente);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<Page<DatosListadoPacientes>> listadoPacientes(
|
public ResponseEntity<Page<DatosListadoPacientes>> listadoPacientes(@PageableDefault(size = 5) Pageable paginacion) {
|
||||||
@PageableDefault(size = 5) Pageable paginacion) {
|
//return pacienteRepository.findAll(paginacion).map(DatosListadoPacientes::new);
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(pacienteRepository.findByActivoTrue(paginacion).map(DatosListadoPacientes::new));
|
||||||
pacienteRepository.findByActivoTrue(paginacion).map(DatosListadoPacientes::new)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping
|
@PutMapping
|
||||||
@Transactional
|
@Transactional
|
||||||
public ResponseEntity<DatosRespuestaPaciente> actualizarPaciente(
|
public ResponseEntity<DatosRespuestaPaciente> actualizarPaciente(@RequestBody @Valid DatosActualizarPaciente datosActualizarPaciente) {
|
||||||
@RequestBody @Valid DatosActualizarPaciente datosActualizarPaciente) {
|
|
||||||
Paciente paciente = pacienteRepository.getReferenceById(datosActualizarPaciente.id());
|
Paciente paciente = pacienteRepository.getReferenceById(datosActualizarPaciente.id());
|
||||||
paciente.actualizarDatos(datosActualizarPaciente);
|
paciente.actualizarDatos(datosActualizarPaciente);
|
||||||
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
|
return ResponseEntity.ok(new DatosRespuestaPaciente(
|
||||||
paciente, new DatosDireccion(paciente.getDireccion())
|
paciente.getId(), paciente.getNombre(), paciente.getEmail(), paciente.getTelefono(), paciente.getDocumento(),
|
||||||
);
|
new DatosDireccion(
|
||||||
return ResponseEntity.ok(datosRespuestaPaciente);
|
paciente.getDireccion().getCalle(), paciente.getDireccion().getDistrito(),
|
||||||
|
paciente.getDireccion().getCiudad(),paciente.getDireccion().getNumero(),
|
||||||
|
paciente.getDireccion().getComplemento()
|
||||||
|
)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desactivar Paciente
|
// Desactivar Paciente
|
||||||
@ -67,10 +72,14 @@ public class PacienteController {
|
|||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<DatosRespuestaPaciente> retornaDatosPaciente(@PathVariable Long id) {
|
public ResponseEntity<DatosRespuestaPaciente> retornaDatosPaciente(@PathVariable Long id) {
|
||||||
Paciente paciente = pacienteRepository.getReferenceById(id);
|
Paciente paciente = pacienteRepository.getReferenceById(id);
|
||||||
DatosRespuestaPaciente datosRespuestaPaciente = new DatosRespuestaPaciente(
|
var datosPaciente = new DatosRespuestaPaciente(
|
||||||
paciente, new DatosDireccion(paciente.getDireccion())
|
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()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok(datosRespuestaPaciente);
|
return ResponseEntity.ok(datosPaciente);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,4 @@ public record DatosDireccion(
|
|||||||
@NotBlank String ciudad,
|
@NotBlank String ciudad,
|
||||||
String numero,
|
String numero,
|
||||||
String complemento) {
|
String complemento) {
|
||||||
|
|
||||||
public DatosDireccion(Direccion direccion){
|
|
||||||
this(direccion.getCalle(),
|
|
||||||
direccion.getDistrito(),
|
|
||||||
direccion.getCiudad(),
|
|
||||||
direccion.getNumero(),
|
|
||||||
direccion.getComplemento()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,4 @@ import med.voll.api.domain.direccion.DatosDireccion;
|
|||||||
public record DatosRespuestaMedico(@NotNull Long id, String nombre,
|
public record DatosRespuestaMedico(@NotNull Long id, String nombre,
|
||||||
String email, String telefono, String documento,
|
String email, String telefono, String documento,
|
||||||
DatosDireccion direccion) {
|
DatosDireccion direccion) {
|
||||||
|
|
||||||
public DatosRespuestaMedico(Medico medico, DatosDireccion direccion){
|
|
||||||
this(medico.getId(),
|
|
||||||
medico.getNombre(),
|
|
||||||
medico.getEmail(),
|
|
||||||
medico.getTelefono(),
|
|
||||||
medico.getDocumento(),
|
|
||||||
direccion);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,4 @@ import med.voll.api.domain.direccion.DatosDireccion;
|
|||||||
public record DatosRespuestaPaciente(@NotNull Long id, String nombre,
|
public record DatosRespuestaPaciente(@NotNull Long id, String nombre,
|
||||||
String email, String telefono, String documento,
|
String email, String telefono, String documento,
|
||||||
DatosDireccion direccion) {
|
DatosDireccion direccion) {
|
||||||
|
|
||||||
public DatosRespuestaPaciente(Paciente paciente, DatosDireccion direccion){
|
|
||||||
this(paciente.getId(),
|
|
||||||
paciente.getNombre(),
|
|
||||||
paciente.getEmail(),
|
|
||||||
paciente.getTelefono(),
|
|
||||||
paciente.getDocumento(),
|
|
||||||
direccion);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package med.voll.api.domain.usuario;
|
|
||||||
|
|
||||||
public record DatosAutenticacionUsuario(String login, String clave) {
|
|
||||||
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
package med.voll.api.infra.errores;
|
package med.voll.api.infra;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
@ -22,12 +21,6 @@ public class ManejadorDeErrores {
|
|||||||
return ResponseEntity.badRequest().body(errores);
|
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) {
|
private record DatosErrorValidacion(String campo, String error) {
|
||||||
public DatosErrorValidacion(FieldError error) {
|
public DatosErrorValidacion(FieldError error) {
|
||||||
this(error.getField(), error.getDefaultMessage());
|
this(error.getField(), error.getDefaultMessage());
|
@ -1,20 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package med.voll.api.infra.security;
|
|
||||||
|
|
||||||
public record DatosJWTtoken(String jwTtoken) {
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
package med.voll.api.infra.security;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
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;
|
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfigurations {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private SecurityFilter securityFilter;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
|
|
||||||
return httpSecurity.csrf().disable().sessionManagement()
|
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and().authorizeHttpRequests()
|
|
||||||
.requestMatchers(HttpMethod.POST, "/login")
|
|
||||||
.permitAll()
|
|
||||||
.anyRequest()
|
|
||||||
.authenticated()
|
|
||||||
.and()
|
|
||||||
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
|
|
||||||
.build();
|
|
||||||
//return httpSecurity.csrf().disable()
|
|
||||||
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
// .and().authorizeHttpRequests()
|
|
||||||
// .requestMatchers(HttpMethod.POST, "/login").permitAll()
|
|
||||||
// .anyRequest().authenticated()
|
|
||||||
// .and().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthenticationManager authenticationManager(
|
|
||||||
AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
|
||||||
return authenticationConfiguration.getAuthenticationManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder () {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package med.voll.api.infra.security;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import med.voll.api.domain.usuario.UsuarioRepository;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class SecurityFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private TokenService tokenService;
|
|
||||||
@Autowired
|
|
||||||
private UsuarioRepository usuarioRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
FilterChain filterChain
|
|
||||||
) throws ServletException, IOException {
|
|
||||||
var authHeader = request.getHeader("Authorization");
|
|
||||||
if (authHeader != null) {
|
|
||||||
System.out.println("- ".repeat(10) + "filtro no null"+" -".repeat(10));
|
|
||||||
var token = authHeader.replace("Bearer ", "");
|
|
||||||
var subject = tokenService.getSubject(token);
|
|
||||||
if (subject != null) {
|
|
||||||
// token válido
|
|
||||||
var usuario = usuarioRepository.findByLogin(subject);
|
|
||||||
var authentication = new UsernamePasswordAuthenticationToken(
|
|
||||||
usuario,
|
|
||||||
null,
|
|
||||||
usuario.getAuthorities() // forzar inicio de sesión
|
|
||||||
);
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
package med.voll.api.infra.security;
|
|
||||||
|
|
||||||
import com.auth0.jwt.JWT;
|
|
||||||
import com.auth0.jwt.JWTVerifier;
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
|
||||||
import com.auth0.jwt.exceptions.JWTCreationException;
|
|
||||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
|
||||||
import med.voll.api.domain.usuario.Usuario;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class TokenService {
|
|
||||||
|
|
||||||
@Value("${api.security.secret}")
|
|
||||||
private String apiSecret;
|
|
||||||
|
|
||||||
public String generarToken(Usuario usuario) {
|
|
||||||
try {
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(apiSecret) ;
|
|
||||||
return JWT.create()
|
|
||||||
.withIssuer("voll med")
|
|
||||||
.withSubject(usuario.getLogin())
|
|
||||||
.withClaim("id", usuario.getId())
|
|
||||||
.withExpiresAt(generarFechaExpiracion())
|
|
||||||
.sign(algorithm);
|
|
||||||
} catch (JWTCreationException exception){
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSubject(String token) {
|
|
||||||
if (token == null) {
|
|
||||||
throw new RuntimeException("token nulo");
|
|
||||||
}
|
|
||||||
DecodedJWT verifier = null;
|
|
||||||
try {
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(apiSecret);
|
|
||||||
verifier = JWT.require(algorithm)
|
|
||||||
// specify an specific claim validations
|
|
||||||
.withIssuer("voll med")
|
|
||||||
// reusable verifier instance
|
|
||||||
.build()
|
|
||||||
.verify(token);
|
|
||||||
verifier.getSubject();
|
|
||||||
} catch (JWTVerificationException exception) {
|
|
||||||
// Invalid signature/claims
|
|
||||||
System.out.println(exception.toString());
|
|
||||||
}
|
|
||||||
if (verifier.getSubject() == null) {
|
|
||||||
throw new RuntimeException("Verifier inválido");
|
|
||||||
}
|
|
||||||
return verifier.getSubject();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Instant generarFechaExpiracion() {
|
|
||||||
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,9 @@
|
|||||||
# suppress inspection "SpellCheckingInspection" for whole file
|
# suppress inspection "SpellCheckingInspection" for whole file
|
||||||
spring.datasource.url=jdbc:mysql://${DB_URL}/vollmed_api2
|
spring.datasource.url=jdbc:mysql://192.168.0.8/vollmed_api2
|
||||||
spring.datasource.username=${DB_USER}
|
spring.datasource.username=alura
|
||||||
spring.datasource.password=${DB_PASS}
|
spring.datasource.password=alura
|
||||||
|
|
||||||
spring.jpa.show-sql=true
|
spring.jpa.show-sql=true
|
||||||
spring.jpa.properties.hibernate.format_sql=true
|
spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
server.error.include-stacktrace=never
|
server.error.include-stacktrace=never
|
||||||
|
|
||||||
api.security.secret=${JWT_SECRET}
|
|
@ -1,9 +0,0 @@
|
|||||||
create table usuarios(
|
|
||||||
|
|
||||||
id bigint not null auto_increment,
|
|
||||||
login varchar(100) not null unique,
|
|
||||||
clave varchar(300) not null,
|
|
||||||
|
|
||||||
primary key(id)
|
|
||||||
|
|
||||||
);
|
|
@ -154,10 +154,12 @@ temporalmente. Las causas comunes son un servidor que está fuera de servicio
|
|||||||
por mantenimiento o sobrecargado. Los ataques maliciosos como ***DDoS***
|
por mantenimiento o sobrecargado. Los ataques maliciosos como ***DDoS***
|
||||||
causan mucho este problema.
|
causan mucho este problema.
|
||||||
|
|
||||||
Para consultar sobre algún código HTTP, se puede usar
|
|
||||||
|
Para consultar sobre algún código HTTP, se puede usar la sgte. página:
|
||||||
|
|
||||||
[http cat](https://http.cat)
|
[http cat](https://http.cat)
|
||||||
|
|
||||||
ejm. consulta por código `405 Method Not Allowed`
|
ejm consulta por código `405 Method Not Allowed`
|
||||||
|
|
||||||
```http
|
```http
|
||||||
https://http.cat/405
|
https://http.cat/405
|
||||||
@ -184,6 +186,8 @@ clase
|
|||||||
[ManejadorDeErrores](./api_rest/api2/src/main/java/med/voll/api/infra/ManejadorDeErrores.java)
|
[ManejadorDeErrores](./api_rest/api2/src/main/java/med/voll/api/infra/ManejadorDeErrores.java)
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
...
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class ManejadorDeErrores {
|
public class ManejadorDeErrores {
|
||||||
|
|
||||||
@ -300,654 +304,3 @@ public record DatosRegistroMedico(
|
|||||||
@Valid DatosDireccion direccion) {}
|
@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)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON Web Token
|
|
||||||
|
|
||||||
[JWT](https://jwt.io) - [Repo](https://github.com/auth0/java-jwt)
|
|
||||||
|
|
||||||
Agregar dependencia a [pom.xml](./api_rest/api2/pom.xml)
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.auth0</groupId>
|
|
||||||
<artifactId>java-jwt</artifactId>
|
|
||||||
<version>4.4.0</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
Creación de clase
|
|
||||||
[TokenService](./api_rest/api2/src/main/java/med/voll/api/infra/security/TokenService.java)
|
|
||||||
en *package* [infra.security](./api_rest/api2/src/main/java/med/voll/api/infra/security/)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Service
|
|
||||||
public class TokenService {
|
|
||||||
|
|
||||||
@Value("${api.security.secret}")
|
|
||||||
private String apiSecret;
|
|
||||||
|
|
||||||
public String generarToken(Usuario usuario) {
|
|
||||||
try {
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(apiSecret) ;
|
|
||||||
return JWT.create()
|
|
||||||
.withIssuer("voll med")
|
|
||||||
.withSubject(usuario.getLogin())
|
|
||||||
.withClaim("id", usuario.getId())
|
|
||||||
.withExpiresAt(generarFechaExpiracion())
|
|
||||||
.sign(algorithm);
|
|
||||||
} catch (JWTCreationException exception){
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private Instant generarFechaExpiracion() {
|
|
||||||
return LocalDateTime.now()
|
|
||||||
.plusHours(2)
|
|
||||||
.toInstant(ZoneOffset.of("-03:00"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Creación de propiedades manejadas por variables de entorno/ambiente
|
|
||||||
|
|
||||||
[application.properties](./api_rest/api2/src/main/resources/application.properties)
|
|
||||||
|
|
||||||
```config
|
|
||||||
spring.datasource.url=jdbc:mysql://${DB_URL}/vollmed_api2
|
|
||||||
spring.datasource.username=${DB_USER}
|
|
||||||
spring.datasource.password=${DB_PASS}
|
|
||||||
|
|
||||||
spring.jpa.show-sql=true
|
|
||||||
spring.jpa.properties.hibernate.format_sql=true
|
|
||||||
|
|
||||||
server.error.include-stacktrace=never
|
|
||||||
|
|
||||||
api.security.secret=${JWT_SECRET}
|
|
||||||
```
|
|
||||||
|
|
||||||
Creación del DTO
|
|
||||||
[DatosTokenJWT](./api_rest/api2/src/main/java/med/voll/api/infra/security/DatosJWTtoken.java)
|
|
||||||
en *package*
|
|
||||||
[infra.security](./api_rest/api2/src/main/java/med/voll/api/infra/security/)
|
|
||||||
|
|
||||||
```java
|
|
||||||
public record DatosJWTtoken(String jwTtoken) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Modificación en 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/)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/login")
|
|
||||||
public class AutenticacionController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private AuthenticationManager authenticationManager;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private TokenService tokenService;
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity autenticarUsuario(
|
|
||||||
@RequestBody @Valid DatosAutenticacionUsuario datosAutenticacionUsuario) {
|
|
||||||
Authentication authtoken = new UsernamePasswordAuthenticationToken(
|
|
||||||
datosAutenticacionUsuario.login(),
|
|
||||||
datosAutenticacionUsuario.clave());
|
|
||||||
var usuarioAutenticado = authenticationManager.authenticate(authtoken);
|
|
||||||
var JWTtoken = tokenService.generarToken(
|
|
||||||
(Usuario) usuarioAutenticado.getPrincipal()
|
|
||||||
);
|
|
||||||
return ResponseEntity.ok(new DatosJWTtoken(JWTtoken));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Control de acceso
|
|
||||||
|
|
||||||
### Filters
|
|
||||||
|
|
||||||
**Filter** es una de las características que componen la especificación
|
|
||||||
Servlets, que estandariza el manejo de solicitudes y respuestas en aplicaciones
|
|
||||||
web en Java. Es decir, dicha función no es específica de Spring y, por lo tanto,
|
|
||||||
puede usarse en cualquier aplicación Java.
|
|
||||||
|
|
||||||
Es una característica muy útil para aislar códigos de infraestructura de la
|
|
||||||
aplicación, como por ejemplo, seguridad, logs y auditoría, para que dichos
|
|
||||||
códigos no se dupliquen y se mezclen con códigos relacionados con las reglas
|
|
||||||
comerciales de la aplicación.
|
|
||||||
|
|
||||||
Para crear un **Filter**, simplemente cree una clase e implemente la interfaz
|
|
||||||
Filter en ella (paquete `jakarta.servlet`). Por ejemplo:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@WebFilter(urlPatterns = "/api/**")
|
|
||||||
public class LogFilter implements Filter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest servletRequest,
|
|
||||||
ServletResponse servletResponse,
|
|
||||||
FilterChain filterChain
|
|
||||||
) throws IOException, ServletException {
|
|
||||||
System.out.println("Solicitud recibida el: " + LocalDateTime.now());
|
|
||||||
filterChain.doFilter(servletRequest, servletResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
El método `doFilter` es llamado por el servidor automáticamente, cada vez que
|
|
||||||
este filter tiene que ser ejecutado, y la llamada al método `filterChain.doFilter`
|
|
||||||
indica que los siguientes filters, si hay otros, pueden ser ejecutados. La
|
|
||||||
anotación `@WebFilter`, agregada a la clase, indica al servidor en qué
|
|
||||||
solicitudes se debe llamar a este filter, según la URL de la solicitud.
|
|
||||||
|
|
||||||
En este proyecto se utiliza otra forma de implementar un filter, utilizando los
|
|
||||||
recursos de Spring que facilitan su implementación.
|
|
||||||
|
|
||||||
|
|
||||||
#### Importante!
|
|
||||||
|
|
||||||
En la versión final `3.0.0` de Spring Boot se realizó un cambio en Spring Security,
|
|
||||||
en cuanto a códigos que restringen el control de acceso. A lo largo de las clases,
|
|
||||||
el método `securityFilterChain(HttpSecurity http)`, declarado en la clase
|
|
||||||
`SecurityConfigurations`, tenía la siguiente estructura:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http)
|
|
||||||
throws Exception {
|
|
||||||
return http.csrf().disable()
|
|
||||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and().authorizeRequests()
|
|
||||||
.antMatchers(HttpMethod.POST, "/login").permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
.and().build();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Sin embargo, desde la versión final `3.0.0` de Spring Boot, el método
|
|
||||||
`authorizeRequests()` ha quedado obsoleto y debe ser reemplazado por el nuevo
|
|
||||||
método `authorizeHttpRequests()`. Asimismo, el método `antMatchers(`) debería
|
|
||||||
ser reemplazado por el nuevo método `requestMatchers()`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
return http.csrf().disable()
|
|
||||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and().authorizeHttpRequests()
|
|
||||||
.requestMatchers(HttpMethod.POST, "/login").permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
.and().build();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Control de acceso
|
|
||||||
|
|
||||||
Creación de clase
|
|
||||||
[SecurityFilter](./api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java)
|
|
||||||
, responsable de interceptar solicitures y realizar
|
|
||||||
el proceso de autenticación y autorización
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class SecurityFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private TokenService tokenService;
|
|
||||||
@Autowired
|
|
||||||
private UsuarioRepository usuarioRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
FilterChain filterChain)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
var authHeader = request.getHeader("Authorization");
|
|
||||||
if (authHeader != null) {
|
|
||||||
var token = authHeader.replace("Bearer ", "");
|
|
||||||
var subject = tokenService.getSubject(token);
|
|
||||||
if (subject != null) {
|
|
||||||
// token válido
|
|
||||||
var usuario = usuarioRepository.findByLogin(subject);
|
|
||||||
var authentication = new UsernamePasswordAuthenticationToken(
|
|
||||||
usuario,
|
|
||||||
null,
|
|
||||||
usuario.getAuthorities() // forzar inicio de sesión
|
|
||||||
);
|
|
||||||
SecurityContextHolder
|
|
||||||
.getContext().setAuthentication(authentication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Actualización en clase
|
|
||||||
[SecurityConfigurations](./api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class SecurityConfigurations {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private SecurityFilter securityFilter;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
|
|
||||||
throws Exception {
|
|
||||||
return httpSecurity.csrf().disable().sessionManagement()
|
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and().authorizeHttpRequests()
|
|
||||||
.requestMatchers(HttpMethod.POST, "/login")
|
|
||||||
.permitAll()
|
|
||||||
.anyRequest()
|
|
||||||
.authenticated()
|
|
||||||
.and()
|
|
||||||
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthenticationManager authenticationManager(
|
|
||||||
AuthenticationConfiguration authenticationConfiguration)
|
|
||||||
throws Exception {
|
|
||||||
return authenticationConfiguration.getAuthenticationManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder () {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Actualización de la clase
|
|
||||||
[TokenService](./api_rest/api2/src/main/java/med/voll/api/infra/security/TokenService.java)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Service
|
|
||||||
public class TokenService {
|
|
||||||
|
|
||||||
@Value("${api.security.secret}")
|
|
||||||
private String apiSecret;
|
|
||||||
|
|
||||||
public String generarToken(Usuario usuario) {
|
|
||||||
try {
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(apiSecret) ;
|
|
||||||
return JWT.create()
|
|
||||||
.withIssuer("voll med")
|
|
||||||
.withSubject(usuario.getLogin())
|
|
||||||
.withClaim("id", usuario.getId())
|
|
||||||
.withExpiresAt(generarFechaExpiracion())
|
|
||||||
.sign(algorithm);
|
|
||||||
} catch (JWTCreationException exception){
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSubject(String token) {
|
|
||||||
if (token == null) {
|
|
||||||
throw new RuntimeException("token nulo");
|
|
||||||
}
|
|
||||||
DecodedJWT verifier = null;
|
|
||||||
try {
|
|
||||||
Algorithm algorithm = Algorithm.HMAC256(apiSecret);
|
|
||||||
verifier = JWT.require(algorithm)
|
|
||||||
// specify an specific claim validations
|
|
||||||
.withIssuer("voll med")
|
|
||||||
// reusable verifier instance
|
|
||||||
.build()
|
|
||||||
.verify(token);
|
|
||||||
verifier.getSubject();
|
|
||||||
} catch (JWTVerificationException exception) {
|
|
||||||
// Invalid signature/claims
|
|
||||||
System.out.println(exception.toString());
|
|
||||||
}
|
|
||||||
if (verifier.getSubject() == null) {
|
|
||||||
throw new RuntimeException("Verifier inválido");
|
|
||||||
}
|
|
||||||
return verifier.getSubject();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Instant generarFechaExpiracion() {
|
|
||||||
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
En este proyecto no se tienen diferentes perfiles de acceso para los usuarios.
|
|
||||||
Sin embargo, esta característica se usa en algunas aplicaciones y se puede
|
|
||||||
configurar **Spring Security** que solo los usuarios que tienen un perfil
|
|
||||||
específico pueden acceder a ciertas URL.
|
|
||||||
|
|
||||||
P.e., suponiendo que en la aplicación tiene un perfil de acceso llamado
|
|
||||||
**ADMIN**, y solo los usuarios con ese perfil pueden eliminar médicos y
|
|
||||||
pacientes. Podemos indicar dicha configuración a **Spring Security**
|
|
||||||
cambiando el método `securityFilterChain`, en la clase
|
|
||||||
`SecurityConfigurations`, de la siguiente manera:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http)
|
|
||||||
throws Exception {
|
|
||||||
return http.csrf().disable()
|
|
||||||
.sessionManagement()
|
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and().authorizeRequests()
|
|
||||||
.antMatchers(HttpMethod.POST, "/login").permitAll()
|
|
||||||
.antMatchers(HttpMethod.DELETE, "/medicos").hasRole("ADMIN")
|
|
||||||
.antMatchers(HttpMethod.DELETE, "/pacientes").hasRole("ADMIN")
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
.and().addFilterBefore(
|
|
||||||
securityFilter,
|
|
||||||
UsernamePasswordAuthenticationFilter.class
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Tener en cuenta que se agregaron dos líneas al código anterior, indicando a
|
|
||||||
**Spring Security** que las solicitudes de tipo **DELETE** de las URL `/médicos`
|
|
||||||
y `/pacientes` solo pueden ser ejecutadas por usuarios autenticados y cuyo perfil
|
|
||||||
de acceso es **ADMIN**.
|
|
||||||
|
|
||||||
### Control de acceso a anotaciones
|
|
||||||
|
|
||||||
Otra forma de restringir el acceso a ciertas funciones, según el perfil del
|
|
||||||
usuario, es usar una función de Spring Security conocida como Method Security,
|
|
||||||
que funciona con el uso de anotaciones en los métodos:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@Secured("ROLE_ADMIN")
|
|
||||||
public ResponseEntity detallar(@PathVariable Long id) {
|
|
||||||
var medico = repository.getReferenceById(id);
|
|
||||||
return ResponseEntity.ok(new DatosDetalladoMedico(medico));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
En el ejemplo de código anterior, el método se anotó con
|
|
||||||
`@Secured("ROLE_ADMIN")`, de modo que sólo los usuarios con el rol **ADMIN**
|
|
||||||
pueden activar solicitudes para detallar a un médico. La anotación `@Secured`
|
|
||||||
se puede agregar en métodos individuales o incluso en la clase, lo que sería
|
|
||||||
el equivalente a agregarla en todos los métodos.
|
|
||||||
|
|
||||||
***¡Atención!*** Por defecto esta característica está deshabilitada en
|
|
||||||
**Spring Security**, y para usarla debemos agregar la siguiente anotación en la
|
|
||||||
clase `Securityconfigurations` del proyecto:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@EnableMethodSecurity(securedEnabled = true)
|
|
||||||
```
|
|
||||||
|
|
||||||
Más detalles sobre la función de seguridad del método en la
|
|
||||||
documentación de
|
|
||||||
[Spring Security](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html)
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user