Compare commits

..

3 Commits

19 changed files with 1059 additions and 60 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,21 @@
<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>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,38 @@
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));
}
}

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,4 @@
package med.voll.api.infra.security;
public record DatosJWTtoken(String jwTtoken) {
}

View File

@ -0,0 +1,55 @@
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();
}
}

View File

@ -0,0 +1,48 @@
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);
}
}

View File

@ -0,0 +1,64 @@
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"));
}
}

View File

@ -1,9 +1,11 @@
# suppress inspection "SpellCheckingInspection" for whole file
spring.datasource.url=jdbc:mysql://192.168.0.8/vollmed_api2
spring.datasource.username=alura
spring.datasource.password=alura
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}

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

@ -154,12 +154,10 @@ 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:
Para consultar sobre algún código HTTP, se puede usar
[http cat](https://http.cat)
ejm consulta por código `405 Method Not Allowed`
ejm. consulta por código `405 Method Not Allowed`
```http
https://http.cat/405
@ -186,8 +184,6 @@ clase
[ManejadorDeErrores](./api_rest/api2/src/main/java/med/voll/api/infra/ManejadorDeErrores.java)
```java
...
@RestControllerAdvice
public class ManejadorDeErrores {
@ -304,3 +300,654 @@ 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)
);
```
## 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)