Olá pessoal!
Neste artigo, demonstrarei os componentes básicos para criar serviços de misturador RESTful reativo usando Spring WebFlux, Spring Security, Spring Cloud Netflix Eureka (descoberta de serviço), Hystrix (disjuntor), faixa de opções (balanceador de carga no lado do cliente), configuração externa (via repositório git) , Spring Cloud Sleuth, Spring Cloud Gateway, MongoDB de inicialização por mola. Assim como o Spring Boot Admin e o Zipkin para monitoramento.
Esta revisão foi feita após o estudo dos livros Spring Microservices in Action e Hands-On Spring 5 Security for Applications Reactive.
Neste artigo, criaremos um aplicativo elementar com três consultas: obter uma lista de jogos, obter uma lista de jogadores, criar um jogo a partir do ID dos jogadores, uma solicitação para verificar a reversão (fallback Hystrix) em caso de uma longa espera por uma resposta. E a implementação da autenticação por meio do token JWT, com base no livro Hands-On Spring 5 Security for Reactive Applications.
Não descreverei como cada aplicativo é criado no IDE, pois este artigo é destinado a um usuário experiente.
Estrutura do projeto

O projeto consiste em dois módulos. O módulo spring-servers
pode ser copiado com segurança de um projeto para outro. Quase não há código e configurações. O módulo tictactoe-services
contém os módulos e microsserviços de nossa aplicação. Notarei imediatamente que, adicionando auth-module
domain-module
aos serviços, viole um dos princípios da arquitetura de microsserviços sobre a autonomia dos microsserviços. Mas, na fase de desenvolvimento desses módulos, acredito que esta é a solução mais ideal.
Configuração Gradle
Eu gosto quando toda a configuração do Gradle está em um arquivo, então eu configurei o projeto inteiro em um 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') } }
O uso de um arquivo de configuração comum permite tornar dependências comuns aos microsserviços, nesse caso, serviços com um nome que termina em "serviço", em um só lugar. MAS, isso novamente viola o princípio da autonomia dos microsserviços. Além de dependências comuns, você pode adicionar tarefas aos subprojetos. gradle.plugin.com.palantir.gradle.docker:gradle-docker
tarefas do plugin gradle.plugin.com.palantir.gradle.docker:gradle-docker
para trabalhar com o Docker
.
Módulo de autenticação
Agora, considere o módulo de autenticação JWT. Uma descrição do pacote de auth
deste módulo pode ser encontrada no livro de autenticação reativa que mencionei acima.

Ah, config
nos aprofundar no pacote de config
com mais detalhes.
A classe de propriedades "complexas" 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 classe contém propriedades "complexas" para a configuração do banco de dados inMemory.
Classe de configuração do 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; }
No arquivo de recurso, você deve especificar essas variáveis. Na minha configuração, o token fica inoperante após 10 horas.
Classe de configuração MicroserviceServiceJwtAuthWebFilter.java dos correspondentes de filtro
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 a construção, esse filtro passa o serviço para trabalhar com o JWT e uma lista de caminhos que esse filtro processará.
Classe de configuração Reativa Spring Security 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(); } }
Existem três anotações interessantes aqui.
@ConditionalOnProperty(value = "microservice", havingValue = "true")
Uma anotação que conecta este módulo, dependendo da variável de microsserviço no arquivo de configuração especificado na anotação. Isso é necessário para desativar a verificação geral de token em alguns módulos. Nesse aplicativo, este é o webapi-service
web, que possui sua própria implementação do bean SecurityWebFilterChain
.
@PropertySource(value = "classpath:/application.properties")
Essa anotação também permite obter propriedades do serviço principal para o qual este módulo é importado. Em outras palavras, variáveis
@Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls;
Pegue seus valores na configuração de microsserviço do descendente.
E uma anotação que permite anexar anotações de segurança como @PreAuthorize(“hasRole('MY_ROLE')”)
@EnableReactiveMethodSecurity
E neste módulo, é criado o bean SecurityWebFilterChain
, que configura o acesso ao atuador, a URL permitida e a URL na qual o token JWT é verificado. Deve-se observar que o acesso ao filtro de token JWT deve estar aberto.
Configuração SpringWebFluxConfig.java
Nesta configuração, os MapReactiveUserDetailsService
são criados para configurar o atuador e outros usuários do sistema na memória.
@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); }
O ReactiveUserDetailsService
necessário para unir nosso repositório de usuários com o Spring Security
.
@Bean public ReactiveUserDetailsService userDetailsRepository(UserRepository users) { return (email) -> users.findByEmail(email).cast(UserDetails.class); }
Um bean para criar um cliente WebClient
para executar solicitações reativas.
@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 a criação, dois filtros são adicionados. LoadBalancer
e o filtro que pega a instância de Authentication
do contexto ReactiveSecurityContext
e cria um token a partir dele para que o filtro seja autenticado pelo servidor de destino e autorizado de acordo.
E para facilitar o trabalho com o tipo e as datas do MongoDB ObjectId
, adicionei uma lixeira de criação do 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(); }
Serviço de jogo de microsserviço
O serviço de jogo de microsserviço tem a seguinte estrutura:

Como você pode ver, apenas um arquivo de configuração 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; }
Ele contém uma variável com o endereço do user-service
ao user-service
e há duas anotações interessantes:
@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")
Esta anotação é necessária para indicar o repositório do MongoDB para o configurador.
@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})
Esta anotação importa as configurações do auth-module
.
Serviço GameService.java
Este serviço possui apenas o seguinte código interessante:
@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(); }
Esse método lança uma exceção aleatoriamente e o Hystrix, de acordo com a anotação, retorna o resultado do seguinte 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); }
Conforme declarado no livro mencionado acima, se algo estiver quebrado, vamos mostrar dados em cache ou alternativos melhor do que nada.
Serviço Webapi de microsserviço
Esse é um tipo de middleware entre o Gateway e os microsserviços internos que não são visíveis do lado de fora. O objetivo deste serviço é obter uma seleção de outros serviços e formar uma resposta ao usuário em sua base.

Iniciamos a revisão com a configuração.
Configuração 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 ) {
Aqui, criamos um gerenciador de autenticação a partir dos serviços userDetailsService
, que definimos anteriormente no auth-module
.
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
E criamos um filtro com esse gerenciador e também adicionamos um conversor de instância de autenticação para obter os dados do usuário codificados em 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); }) );
Adicionamos um manipulador de autorização bem-sucedido, cuja essência é colocar o token JWT no cabeçalho da solicitação gerado a partir da Authentication
para que a autenticação possa ser feita apenas usando um token convidado válido.
tokenWebFilter.setAuthenticationSuccessHandler(new JwtAuthSuccessHandler(jwtService)); MicroserviceServiceJwtAuthWebFilter webApiJwtServiceWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange()
Resolvemos endereços da lista branca. Como escrevi anteriormente, os endereços que serão processados pelo filtro JWT também devem ser abertos
.pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange()
Protegemos o atuador e alguns endereços com autenticação básica
.pathMatchers("/actuator/**").hasRole("SYSTEM") .pathMatchers(HttpMethod.GET, "/url-protected/**").hasRole("GUEST") .pathMatchers(HttpMethod.POST, "/url-protected/**").hasRole("USER") .and() .httpBasic() .and() .authorizeExchange()
Tornando obrigatório o acesso ao token de autenticação
.pathMatchers(AUTH_TOKEN_PATH).authenticated() .and()
Adicione filtros. Para autenticar e verificar o token JWT.
.addFilterAt(webApiJwtServiceWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAt(tokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); }
E, como escrevi acima, esse serviço desabilita a verificação de token JWT comum para outros serviços, especificando o valor da variável micoservice=false
no arquivo application.properites
.
Controlador de emissão, registro e autorização de token AuthController.java
Não descreverei esse controlador, pois é puramente específico.
Serviço WebApiService.java
Este serviço é chamado no controlador WebApiMethodProtectedController.jav
e possui uma anotação interessante:
@PreAuthorize("hasRole('GUEST')") public Flux<User> getAllUsers() { }
Esta anotação permite o acesso a métodos apenas para usuários autorizados com a função de convidado.
Como testar
Crie um ambiente:

Obter token

Atualize a variável TOKEN no ambiente com o token recebido.
Registrar um novo usuário

Após o registro, você receberá um token de usuário. Ele expira em 10 horas. Quando expirar, você precisará obter um novo. Para fazer isso, solicite o token convidado novamente, atualize o ambiente e execute a solicitação

Em seguida, você pode obter uma lista de usuários, jogos ou criar um novo jogo. E também teste o Hystrix, consulte configurações de serviço e criptografar variáveis para o repositório git.
Referências