Asegurando HTTP Puntos finales

Por defecto, el IPF APIs no autentique las solicitudes entrantes. Esto puede ser configurado en la implementación del cliente incluyendo el spring-boot-starter-security dependencia del proyecto. La documentación completa sobre Spring Security se puede encontrar aquí.

Ejemplo

Esta sección demostrará cómo una implementación de cliente podría agregar la validación de tokens de acceso JWT en los puntos finales expuestos por el ipf-read-starter.

Requisitos previos

La implementación del cliente del lado de lectura debe estar implementada y funcionando, aceptando todas las solicitudes válidas.

Añadir Maven Dependencies

Para este ejemplo, se requieren dos dependencias. El primero es el mencionado anteriormente.spring-boot-starter-security módulo proporcionado por el Spring framework.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

La segunda es una biblioteca JWT que se utilizará para validar tokens en las solicitudes entrantes.

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
</dependency>

Servicio para Validar JWTs

A continuación, se creará un servicio para validar y decodificar JWTs. El servicio requiere el secreto utilizado para firmar el token para que pueda verificar la firma del token. El secreto se inyecta automáticamente utilizando el @Value anotación proporcionada por Spring.

@Service
public class JWTService {

    private final String secret;

    @Autowired
    public JWTService(@Value("${ipf.security.jwt.secret}") String secret) {
        this.secret = secret;
    }

    public Optional<DecodedJWT> decodeAccessToken(String accessToken) {
        return decode(secret, accessToken);
    }

    public List<GrantedAuthority> getRoles(DecodedJWT decodedJWT) {
        return decodedJWT.getClaim("role").asList(String.class).stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    private Optional<DecodedJWT> decode(String signature, String token) {
        try {
            return Optional.of(JWT.require(Algorithm. HMAC512(signature.getBytes(StandardCharsets. UTF_8)))
                    .build()
                    .verify(token.replace("Bearer ", "")));
        } catch (Exception ex) {
            return Optional.empty();
        }
    }
}

Componente de Filtro de Autorización

Cuando se realiza una solicitud, esta se transmite a través de una cadena de WebFilter clases. La mayor parte de la cadena es proporcionada por Spring de forma predeterminada, aunque el cliente puede proporcionar filtros adicionales. En este ejemplo, se implementará un WebFilter para validar que las solicitudes realizadas a los puntos finales seguros hayan establecido el Authorization Encabezado con un token válido, de lo contrario la solicitud debe fallar (a menos que se proporcione otro filtro de autenticación que permita que las solicitudes sean autenticadas de una manera diferente).

@Component
public class JWTAuthorizationFilter implements WebFilter {

    private final JWTService jwtService;

    public JWTAuthorizationFilter(JWTService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, @NotNull WebFilterChain chain) {
        String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders. AUTHORIZATION);
        if (header == null ||! header.startsWith("Bearer ")) {
            return chain.filter(exchange);
        }

        return jwtService.decodeAccessToken(header)
                .map(token -> new UsernamePasswordAuthenticationToken(
                        token.getSubject(),
                        null,
                        jwtService.getRoles(token)))
                .map(token -> chain.filter(exchange)
                        .subscriberContext(context -> ReactiveSecurityContextHolder.withAuthentication(token)))
                .orElse(chain.filter(exchange)
                        .subscriberContext(context -> ReactiveSecurityContextHolder.clearContext().apply(context)));
    }
}

El JWTAuthorizationFilter implementa el WebFilter método único de la clase filter. Verifica que la solicitud de Authorization el encabezado para está precedido por "Bearer " seguido de un JWT válido.

  • Si el encabezado no está configurado, el filtro simplemente pasa el intercambio al siguiente filtro sin realizar ninguna acción.

  • Si el token es válido, el filtro establece la autenticación en el contexto de seguridad.

  • De lo contrario, el filtro borra el contexto de seguridad.

Configure el SecurityWebFilterChain

Finalmente, el SecurityWebFilterChain debe ser configurado como un Spring bean.

@Configuration
@EnableWebFluxSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityWebFilterChain secureFilterChain(ServerHttpSecurity http,
                                                    JWTAuthorizationFilter jwtAuthorizationFilter) {
        return http
                .formLogin(ServerHttpSecurity. FormLoginSpec::disable)
                .httpBasic(ServerHttpSecurity. HttpBasicSpec::disable)
                .csrf(ServerHttpSecurity. CsrfSpec::disable)
                .logout(ServerHttpSecurity. LogoutSpec::disable)
                .authorizeExchange(spec -> spec
                        .pathMatchers("/actuator/health").permitAll()
                        .pathMatchers("/actuator/**").hasAnyRole("ADMIN")
                        .anyExchange().authenticated()
                )
                .addFilterAt(jwtAuthorizationFilter, SecurityWebFiltersOrder. AUTHORIZATION)
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .build();
    }

    @Bean
    public AbstractJackson2Decoder jacksonDecoder(ObjectMapper mapper) {
        return new Jackson2JsonDecoder(mapper);
    }

}

Esta configuración está diseñada para deshabilitar algunas funciones que no son necesarias (inicio de sesión en el formulario,http básico, csrf y cerrar sesión). Los dos aspectos más importantes son el authorizeExchange especificación y addFilterAt método.

El authorizeExchange El bloque especifica un número de comparadores de ruta que determinarán qué se debe hacer con las solicitudes que coincidan con esos comparadores.

  • El primer comparador asegura que cualquier solicitud realizada a "/actuator/health" se permitirá, es decir, no se requiere un token de autorización.

  • El segundo comparador solo permitirá solicitudes realizadas a "/actuator/**" que tienen el rol "ADMIN". Esto requiere implícitamente que el token también sea válido.

  • El último es un emparejador general que garantizará que cualquier solicitud no especificada esté autenticada, es decir, que tenga un token válido.

El addFilterAt método simplemente añade el JWTAuthorizationFilter que fue creado previamente en la cadena de filtros y lo coloca en el AUTHORIZATION etapa dentro de la cadena. Esto asegura que la solicitud se envíe al filtro en el momento adecuado.

Pruebas

Para probar la configuración recién añadida, el ipf.security.jwt.secret la propiedad debe ser establecida. En este ejemplo, el valor secret se utilizará por simplicidad.

ipf.security.jwt.secret=secret

Se requiere una herramienta para crear JWTs válidos, la herramienta utilizada en esta demostración se puede encontrar en este Repositorio de GitHub.

Establezca las siguientes variables de entorno.

JWT_SECRET=secret
JWT_EXPIRY_SECONDS=180 # 3 minutes
JWT_ROLE=USER

Luego, cree el JWT con el siguiente comando.

JWT=$(jwt encode \
  --alg HS512 \
  --sub user \
  --exp $(expr $(date +%s) + ${JWT_EXPIRY_SECONDS}) \
  --secret ${JWT_SECRET} \
  '{"role": ["ROLE_${ROLE}"]}')

En este momento, el servicio debe estar iniciado, y la configuración puede ser probada utilizando curl.

# Verify the "/actuator/health" endpoint does not need to be authenticated
curl localhost:8080/actuator/health # should return 200

# Verify the "/transactions" endpoint needs to be authenticated
curl localhost:8080/transactions -i # should return 401
curl -H "Authorization: Bearer $JWT" localhost:8080/transactions # Should return 200

# Verify the "/actuator/info" endpoint needs to be authenticated and have role ADMIN
curl localhost:8080/actuator/info -i # should return 401
curl -H "Authorization: Bearer $JWT" localhost:8080/actuator/info -i # should return 401
JWT_ROLE=ADMIN
JWT=<same as before>
curl -H "Authorization: Bearer $JWT" localhost:8080/actuator/info -i # should return 200