From dfd811fd81b02838b910c5f65e68b82db35fad11 Mon Sep 17 00:00:00 2001 From: devfzn Date: Wed, 13 Sep 2023 21:20:10 -0300 Subject: [PATCH] =?UTF-8?q?Spring=20Boot=203=20-=20Buenas=20pr=C3=A1cticas?= =?UTF-8?q?=20y=20protecci=C3=B3n:=20Aula=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FIN Curso --- .../controller/AutenticacionController.java | 2 +- .../security/SecurityConfigurations.java | 22 +- .../api/infra/security/SecurityFilter.java | 48 +++ .../voll/api/infra/security/TokenService.java | 26 ++ 010_spring_boot/spring_boot_2.md | 295 +++++++++++++++++- 5 files changed, 385 insertions(+), 8 deletions(-) create mode 100644 010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java diff --git a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/controller/AutenticacionController.java b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/controller/AutenticacionController.java index b877e0d..53d93ba 100644 --- a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/controller/AutenticacionController.java +++ b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/controller/AutenticacionController.java @@ -33,6 +33,6 @@ public class AutenticacionController { var usuarioAutenticado = authenticationManager.authenticate(authtoken); var JWTtoken = tokenService.generarToken((Usuario) usuarioAutenticado.getPrincipal()); return ResponseEntity.ok(new DatosJWTtoken(JWTtoken)); - } + } diff --git a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java index a5b1137..3ef98a2 100644 --- a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java +++ b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java @@ -1,7 +1,9 @@ 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; @@ -10,15 +12,33 @@ 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().build(); + .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 diff --git a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java new file mode 100644 index 0000000..a6da087 --- /dev/null +++ b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/SecurityFilter.java @@ -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); + } + +} diff --git a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/TokenService.java b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/TokenService.java index 639c2ff..bd8fde0 100644 --- a/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/TokenService.java +++ b/010_spring_boot/api_rest/api2/src/main/java/med/voll/api/infra/security/TokenService.java @@ -1,8 +1,11 @@ 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; @@ -29,7 +32,30 @@ public class TokenService { } 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() { diff --git a/010_spring_boot/spring_boot_2.md b/010_spring_boot/spring_boot_2.md index c012aa7..e7f1d7d 100644 --- a/010_spring_boot/spring_boot_2.md +++ b/010_spring_boot/spring_boot_2.md @@ -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 { @@ -668,3 +664,290 @@ public class AutenticacionController { } ``` +## 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) +