Hola a todos!
En este artículo, demostraré los componentes básicos para crear servicios de mezcladores RESTful reactivos usando Spring WebFlux, Spring Security, Spring Cloud Netflix Eureka (Service Discovery), Hystrix (Circuit Breaker), Ribbon (Client Side Load Balancer), Configuración externa (a través del repositorio git) , Spring Cloud Sleuth, Spring Cloud Gateway, Spring Boot Reactive MongoDB. Además de Spring Boot Admin y Zipkin para el monitoreo.
Esta revisión se realizó después de estudiar los libros Spring Microservices in Action y Hands-On Spring 5 Security for Reactive Applications.
En este artículo, crearemos una aplicación primaria con tres consultas: obtener una lista de juegos, obtener una lista de jugadores, crear un juego desde la identificación de los jugadores, una solicitud para verificar el retroceso (Hystrix fallback) en caso de una larga espera para obtener una respuesta. Y la implementación de la autenticación a través del token JWT basado en el libro Hands-On Spring 5 Security para aplicaciones reactivas.
No describiré cómo se crea cada aplicación en el IDE, ya que este artículo está destinado a un usuario experimentado.
Estructura del proyecto

El proyecto consta de dos módulos. El módulo de spring-servers
se puede copiar de forma segura de un proyecto a otro. Casi no hay código y configuraciones. El tictactoe-services
contiene los módulos y microservicios de nuestra aplicación. Notaré de inmediato que al agregar auth-module
domain-module
a los servicios, infringe uno de los principios de la arquitectura de microservicios sobre la autonomía de los microservicios. Pero en la etapa de desarrollo de estos módulos, creo que esta es la solución más óptima.
Configuración de Gradle
Me gusta cuando toda la configuración de Gradle está en un archivo, así que configuré todo el proyecto en un build.gradle
.
build.gradle buildscript { ext { springBootVersion = '2.1.1.RELEASE' gradleDockerVersion = '0.20.1' } repositories { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("gradle.plugin.com.palantir.gradle.docker:gradle-docker:${gradleDockerVersion}") } } allprojects { group = 'com.tictactoe' apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'com.palantir.docker' apply plugin: 'com.palantir.docker-run' apply plugin: 'com.palantir.docker-compose' } docker.name = 'com.tictactoe' bootJar.enabled = false sourceCompatibility = 11 repositories { mavenCentral() maven { url "https://repo.spring.io/milestone" } } subprojects { ext['springCloudVersion'] = 'Greenwich.M3' sourceSets.configureEach { sourceSet -> tasks.named(sourceSet.compileJavaTaskName, { options.annotationProcessorGeneratedSourcesDirectory = file("$buildDir/generated/sources/annotationProcessor/java/${sourceSet.name}") }) } repositories { mavenCentral() maven { url "https://repo.spring.io/milestone" } } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compileOnly('org.projectlombok:lombok') annotationProcessor('org.projectlombok:lombok') } } project(':spring-servers') { bootJar.enabled = false task cleanAll { dependsOn subprojects*.tasks*.findByName('clean') } task buildAll { dependsOn subprojects*.tasks*.findByName('build') } dockerCompose { template 'docker-compose.spring-servers.template.yml' dockerComposeFile 'docker-compose.spring-servers.yml' } } project(':tictactoe-services') { bootJar.enabled = false task cleanAll { dependsOn subprojects*.tasks*.findByName('clean') } task buildAll { dependsOn subprojects*.tasks*.findByName('build') } }
El uso de un archivo de configuración común le permite hacer que las dependencias sean comunes a los microservicios, en este caso los servicios con un nombre que termina en "servicio", en un solo lugar. PERO, esto viola nuevamente el principio de autonomía de los microservicios. Además de las dependencias comunes, puede agregar tareas a subproyectos. gradle.plugin.com.palantir.gradle.docker:gradle-docker
tareas del complemento gradle.plugin.com.palantir.gradle.docker:gradle-docker
para trabajar con Docker
.
Módulo de autenticación
Ahora, considere el módulo de autenticación JWT. Se puede encontrar una descripción del paquete de auth
de este módulo en el libro de autenticación reactiva que mencioné anteriormente.

Ah, detengámonos en el paquete de config
con más detalle.
La clase de propiedades "complejas" ApplicationClientsProperties.java
@Data @Component @ConfigurationProperties("appclients") public class ApplicationClientsProperties { private List<ApplicationClient> clients = new ArrayList<>(); @Data public static class ApplicationClient { private String username; private String password; private String[] roles; } }
Esta clase contiene propiedades "complejas" para la configuración de la base de datos inMemory.
Clase de configuración del módulo AuthModuleConfig.java
@Data @Configuration @PropertySource("classpath:moduleConfig.yml") public class AuthModuleConfig { @Value("${tokenExpirationMinutes:60}") private Integer tokenExpirationMinutes; @Value("${tokenIssuer:workingbit-example.com}") private String tokenIssuer; @Value("${tokenSecret:secret}") // length minimum 256 bites private String tokenSecret; }
En el archivo de recursos, debe especificar estas variables. En mi configuración, el token se apaga después de 10 horas.
MicroserviceServiceJwtAuthWebFilter.java Clase de configuración de filtros de matchers
public class MicroserviceServiceJwtAuthWebFilter extends JwtAuthWebFilter { private final String[] matchersStrings; public MicroserviceServiceJwtAuthWebFilter(JwtService jwtService, String[] matchersStrings) { super(jwtService); this.matchersStrings = matchersStrings; } @Override protected ServerWebExchangeMatcher getAuthMatcher() { List<ServerWebExchangeMatcher> matchers = Arrays.stream(this.matchersStrings) .map(PathPatternParserServerWebExchangeMatcher::new) .collect(Collectors.toList()); return ServerWebExchangeMatchers.matchers(new OrServerWebExchangeMatcher(matchers)); } }
Durante la construcción, este filtro pasa el servicio para trabajar con JWT y una lista de rutas que procesará este filtro.
Clase de configuración Reactive Spring Boot Security MicroserviceSpringSecurityWebFluxConfig.java
@ConditionalOnProperty(value = "microservice", havingValue = "true") @EnableReactiveMethodSecurity @PropertySource(value = "classpath:/application.properties") public class MicroserviceSpringSecurityWebFluxConfig { @Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls; /** * Bean which configures whiteListed and JWT filter urls * Also it configures authentication for Actuator. Actuator takes configured AuthenticationManager automatically * which uses MapReactiveUserDetailsService to configure inMemory users */ @Bean public SecurityWebFilterChain springSecurityFilterChain( ServerHttpSecurity http, JwtService jwtService ) { MicroserviceServiceJwtAuthWebFilter userServiceJwtAuthWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange() .pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange() .pathMatchers("/actuator/**").hasRole("SYSTEM") .and() .httpBasic() .and() .addFilterAt(userServiceJwtAuthWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); } }
Aquí hay tres anotaciones interesantes.
@ConditionalOnProperty(value = "microservice", havingValue = "true")
Una anotación que conecta este módulo en función de la variable de microservicio en el archivo de configuración que se especifica en la anotación. Esto es necesario para deshabilitar la comprobación general de tokens en algunos módulos. En esta aplicación, este es el webapi-service
, que tiene su propia implementación del bean SecurityWebFilterChain
.
@PropertySource(value = "classpath:/application.properties")
Esta anotación también le permite tomar propiedades del servicio principal al que se importa este módulo. En otras palabras, variables
@Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls;
Tome sus valores de la configuración de microservicio del descendiente.
Y, una anotación que le permite adjuntar anotaciones de seguridad como @PreAuthorize(“hasRole('MY_ROLE')”)
@EnableReactiveMethodSecurity
Y en este módulo, se crea el bean SecurityWebFilterChain
, que configura el acceso al actuador, la url permitida y la url en la que se comprueba el token JWT. Cabe señalar que el acceso al filtro de token JWT debe estar abierto.
Configuración SpringWebFluxConfig.java
En esta configuración, los MapReactiveUserDetailsService
se crean para configurar el actuador y otros usuarios del sistema en la memoria.
@Bean @Primary public MapReactiveUserDetailsService userDetailsRepositoryInMemory() { List<UserDetails> users = applicationClients.getClients() .stream() .map(applicationClient -> User.builder() .username(applicationClient.getUsername()) .password(passwordEncoder().encode(applicationClient.getPassword())) .roles(applicationClient.getRoles()).build()) .collect(toList()); return new MapReactiveUserDetailsService(users); }
El ReactiveUserDetailsService
que es necesario para unir nuestro repositorio de usuarios con Spring Security
.
@Bean public ReactiveUserDetailsService userDetailsRepository(UserRepository users) { return (email) -> users.findByEmail(email).cast(UserDetails.class); }
Un bean para crear un cliente WebClient
para realizar solicitudes reactivas.
@Bean public WebClient loadBalancedWebClientBuilder(JwtService jwtService) { return WebClient.builder() .filter(lbFunction) .filter(authorizationFilter(jwtService)) .build(); } private ExchangeFilterFunction authorizationFilter(JwtService jwtService) { return ExchangeFilterFunction .ofRequestProcessor(clientRequest -> ReactiveSecurityContextHolder.getContext() .map(securityContext -> ClientRequest.from(clientRequest) .header(HttpHeaders.AUTHORIZATION, jwtService.getHttpAuthHeaderValue(securityContext.getAuthentication())) .build())); }
Durante la creación, se agregan dos filtros. LoadBalancer
y el filtro que toma la instancia de Authentication
del contexto ReactiveSecurityContext
y crea un token a partir de él para que el servidor de destino autentique el filtro y lo autorice en consecuencia.
Y para la conveniencia de trabajar con el tipo y las fechas de ObjectId
MongoDB, agregué un bin de creación de objectMapper:
@Bean @Primary ObjectMapper objectMapper() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); builder.serializerByType(ObjectId.class, new ToStringSerializer()); builder.deserializerByType(ObjectId.class, new JsonDeserializer() { @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Map oid = p.readValueAs(Map.class); return new ObjectId( (Integer) oid.get("timestamp"), (Integer) oid.get("machineIdentifier"), ((Integer) oid.get("processIdentifier")).shortValue(), (Integer) oid.get("counter")); } }); builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return builder.build(); }
Servicio de juegos de microservicio
El servicio de juegos de microservicios tiene la siguiente estructura:

Como puede ver en él, solo un archivo de configuración de ApplicationConfig
Configurador ApplicationConfig.java
@Data @Configuration @EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository") @Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class}) public class ApplicationConfig { @Value("${userserviceUrl}") private String userServiceUrl; }
Contiene una variable con la dirección del user-service
al user-service
y hay dos anotaciones interesantes:
@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")
Esta anotación es necesaria para indicar el repositorio de MongoDB al configurador.
@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})
Esta anotación importa las configuraciones del auth-module
.
Servicio GameService.java
Este servicio solo tiene el siguiente código interesante:
@HystrixCommand public Flux<Game> getAllGames() { return gameRepository.findAll(); } @HystrixCommand(fallbackMethod = "buildFallbackAllGames", threadPoolKey = "licenseByOrgThreadPool", threadPoolProperties = {@HystrixProperty(name = "coreSize", value = "30"), @HystrixProperty(name = "maxQueueSize", value = "10")}, commandProperties = { @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "75"), @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "7000"), @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "15000"), @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "5")} ) public Flux<Game> getAllGamesLong() { // logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId()); randomlyRunLong(); return gameRepository.findAll(); }
Este método arroja una excepción al azar y Hystrix de acuerdo con la anotación devuelve el resultado del siguiente método:
private Flux<Game> buildFallbackAllGames() { User fakeUserBlack = new User("fakeUserBlack", "password", Collections.emptyList()); User fakeUserWhite = new User("fakeUserBlack", "password", Collections.emptyList()); Game game = new Game(fakeUserBlack, fakeUserWhite); List<Game> games = List.of(game); return Flux.fromIterable(games); }
Como se indica en el libro mencionado anteriormente, si algo está roto, entonces vamos a mostrar los datos en caché o alternativos mejor que nada.
Servicio de microservicio webapi
Este es un tipo de middleware entre el Gateway y los microservicios internos que no son visibles desde el exterior. El propósito de este servicio es obtener una selección de otros servicios y formar una respuesta al usuario sobre la base.

Comenzamos la revisión con la configuración.
Configuración de SpringSecurityWebFluxConfig.java
@Configuration @EnableReactiveMethodSecurity public class SpringSecurityWebFluxConfig { private static final String AUTH_TOKEN_PATH = "/auth/token"; @Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls; @Bean @Primary public SecurityWebFilterChain systemSecurityFilterChain( ServerHttpSecurity http, JwtService jwtService, @Qualifier("userDetailsRepository") ReactiveUserDetailsService userDetailsService ) {
Aquí creamos un administrador de autenticación a partir de los servicios userDetailsService
, que definimos anteriormente en el auth-module
.
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
Y creamos un filtro con este administrador, y también agregamos un convertidor de instancia de autenticación para obtener los datos del usuario codificados en x-www-form-urlencoded
.
AuthenticationWebFilter tokenWebFilter = new AuthenticationWebFilter(authenticationManager); tokenWebFilter.setServerAuthenticationConverter(exchange -> Mono.justOrEmpty(exchange) .filter(ex -> AUTH_TOKEN_PATH.equalsIgnoreCase(ex.getRequest().getPath().value())) .flatMap(ServerWebExchange::getFormData) .filter(formData -> !formData.isEmpty()) .map((formData) -> { String email = formData.getFirst("email"); String password = formData.getFirst("password"); return new UsernamePasswordAuthenticationToken(email, password); }) );
Agregamos un controlador de autorización exitoso, cuya esencia es colocar el token JWT en el encabezado de solicitud generado a partir de Authentication
para que la autenticación solo se pueda hacer usando un token de invitado válido.
tokenWebFilter.setAuthenticationSuccessHandler(new JwtAuthSuccessHandler(jwtService)); MicroserviceServiceJwtAuthWebFilter webApiJwtServiceWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange()
Resolvemos direcciones de la lista blanca. Como escribí anteriormente, las direcciones que serán procesadas por el filtro JWT también deben abrirse
.pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange()
Protegemos el actuador y algunas direcciones con autenticación básica.
.pathMatchers("/actuator/**").hasRole("SYSTEM") .pathMatchers(HttpMethod.GET, "/url-protected/**").hasRole("GUEST") .pathMatchers(HttpMethod.POST, "/url-protected/**").hasRole("USER") .and() .httpBasic() .and() .authorizeExchange()
Hacer que el acceso al token de autenticación sea obligatorio
.pathMatchers(AUTH_TOKEN_PATH).authenticated() .and()
Agregar filtros Para autenticar y verificar el token JWT.
.addFilterAt(webApiJwtServiceWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAt(tokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); }
Y como escribí anteriormente, este servicio deshabilita la verificación de token JWT, común para otros servicios, al especificar el valor de la variable micoservice=false
en el archivo application.properites
.
Controlador de emisión, registro y autorización de tokens AuthController.java
No describiré este controlador, ya que es puramente específico.
Servicio WebApiService.java
Este servicio se llama en el controlador WebApiMethodProtectedController.jav
y tiene una anotación interesante:
@PreAuthorize("hasRole('GUEST')") public Flux<User> getAllUsers() { }
Esta anotación permite el acceso a métodos solo a usuarios autorizados con el rol de invitado.
Cómo probar
Crea un entorno:

Obtener token

Actualice la variable TOKEN en el entorno con el token recibido.
Registrar un nuevo usuario

Después del registro, recibirá un token de usuario. Caduca en 10 horas. Cuando caduque, debe obtener uno nuevo. Para hacer esto, solicite nuevamente el token de invitado, actualice el entorno y ejecute la solicitud

A continuación, puede obtener una lista de usuarios, juegos o crear un nuevo juego. Y también pruebe Hystrix, vea las configuraciones del servicio y cifre las variables para el repositorio git.
Referencias