大家好!
在本文中,我将演示使用Spring WebFlux,Spring Security,Spring Cloud Netflix Eureka(服务发现),Hystrix(断路器),Ribbon(客户端负载均衡器),外部配置(通过git存储库)创建响应式RESTful混合器服务的基本组件。 ,Spring Cloud Sleuth,Spring Cloud Gateway,Spring Boot Reactive MongoDB。 以及用于监控的Spring Boot Admin和Zipkin。
这篇评论是在学习《 Spring Microservices in Action》和《 Spring 5用于响应式应用程序的安全性》一书之后进行的。
在本文中,我们将创建一个具有三个查询的基本应用程序:获取游戏列表,获取玩家列表,根据玩家ID创建游戏,在长时间等待答案的情况下检查回滚(Hystrix回退)的请求。 并基于《用于响应式应用程序的Spring 5安全性》一书通过JWT令牌实现身份验证。
我不会描述如何在IDE中创建每个应用程序,因为本文是针对有经验的用户的。
项目结构

该项目包括两个模块。 可以在项目之间安全地复制spring-servers
模块。 几乎没有代码和配置。 tictactoe-services
模块包含我们应用程序的模块和微服务。 我将立即注意到,将auth-module
和domain-module
到服务中,违反了关于微服务自治性的微服务架构原则之一。 但是在开发这些模块的阶段,我相信这是最佳的解决方案。
摇篮配置
我喜欢将整个Gradle配置存储在一个文件中,所以我将整个项目配置在一个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') } }
使用通用配置文件可以使微服务(在本例中为名称以“ service”结尾的服务)通用的依赖关系。 但是,这再次违反了微服务自治的原则。 除了常见的依赖关系,您还可以将任务添加到子项目中。 我添加了gradle.plugin.com.palantir.gradle.docker:gradle-docker
插件任务以与Docker
。
身份验证模块
现在,考虑JWT身份验证模块。 可以在我上面提到的反应式认证书中找到该模块的auth
软件包的描述。

嗯, config
更详细地讨论config
包。
“复杂”属性类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; } }
此类包含inMemory数据库配置的“复杂”属性。
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; }
在资源文件中,您必须指定这些变量。 在我的配置中,令牌在10小时后失效。
MicroserviceServiceJwtAuthWebFilter.java筛选器匹配器配置类
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)); } }
在构建期间,此过滤器将传递用于JWT的服务以及此过滤器将处理的路径列表。
响应式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(); } }
这里有三个有趣的注释。
@ConditionalOnProperty(value = "microservice", havingValue = "true")
根据注释中指定的配置文件中的微服务变量连接此模块的注释。 为了禁用某些模块中的常规令牌检查,这是必需的。 在此应用程序中,这是webapi-service
,它具有自己的SecurityWebFilterChain
bean实现。
@PropertySource(value = "classpath:/application.properties")
此注释还允许您从导入了此模块的主要服务中获取属性。 换句话说,变数
@Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls;
从后代的微服务配置中获取它们的值。
并且,允许您附加@PreAuthorize(“hasRole('MY_ROLE')”)
类的安全注释的注释
@EnableReactiveMethodSecurity
并且在此模块中,创建了SecurityWebFilterChain
bean,该bean配置对执行器的访问,允许的URL和检查JWT令牌的URL。 应当注意,必须打开对JWT令牌过滤器的访问。
SpringWebFluxConfig.java配置
在此配置中,将创建MapReactiveUserDetailsService
,以在内存中配置执行器和其他系统用户。
@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); }
ReactiveUserDetailsService
是使用Spring Security
缝合用户存储库所必需的。
@Bean public ReactiveUserDetailsService userDetailsRepository(UserRepository users) { return (email) -> users.findByEmail(email).cast(UserDetails.class); }
用于创建WebClient
客户端以执行响应请求的Bean。
@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())); }
在创建过程中,添加了两个过滤器。 LoadBalancer
和从ReactiveSecurityContext
上下文获取Authentication
实例并从中创建令牌的筛选器,以便筛选器由目标服务器进行身份验证并得到相应的授权。
为了方便使用MongoDB ObjectId
类型和日期,我添加了一个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(); }
微服务游戏服务
微服务游戏服务具有以下结构:

如您所见,只有一个ApplicationConfig配置文件
配置器ApplicationConfig.java
@Data @Configuration @EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository") @Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class}) public class ApplicationConfig { @Value("${userserviceUrl}") private String userServiceUrl; }
它包含一个带有user-service
地址的变量,并且有两个有趣的注释:
@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")
需要此批注以向配置程序指示MongoDB存储库。
@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})
该注释从auth-module
导入配置。
GameService.java服务
该服务仅包含以下有趣的代码:
@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(); }
此方法随机引发异常,并且Hystrix根据注释返回以下方法的结果:
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); }
正如上面提到的书中所述,如果发生故障,那么让我们展示缓存或替代数据总比没有好。
微服务WebAPI服务
这是网关和内部微服务之间的一种中间件,从外部看不到。 该服务的目的是从其他服务中获得选择,并在此基础上形成对用户的响应。

我们从配置开始审核。
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 ) {
在这里,我们从userDetailsService
服务创建了一个身份验证管理器,该服务已在前面的auth-module
定义。
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
然后,我们使用该管理器创建一个过滤器,并添加一个Authentication实例转换器,以获取以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); }) );
我们添加了一个成功的授权处理程序,其本质是将JWT令牌放入Authentication
生成的请求标头中,以便只能使用有效的来宾令牌进行身份验证。
tokenWebFilter.setAuthenticationSuccessHandler(new JwtAuthSuccessHandler(jwtService)); MicroserviceServiceJwtAuthWebFilter webApiJwtServiceWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange()
我们解析白名单中的地址。 就像我之前写的一样,必须由JWT过滤器处理的地址也必须打开
.pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange()
我们通过基本身份验证保护执行器和某些地址
.pathMatchers("/actuator/**").hasRole("SYSTEM") .pathMatchers(HttpMethod.GET, "/url-protected/**").hasRole("GUEST") .pathMatchers(HttpMethod.POST, "/url-protected/**").hasRole("USER") .and() .httpBasic() .and() .authorizeExchange()
强制认证令牌访问
.pathMatchers(AUTH_TOKEN_PATH).authenticated() .and()
添加过滤器。 认证和验证JWT令牌。
.addFilterAt(webApiJwtServiceWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAt(tokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); }
就像我在上面写的,该服务通过在application.properites
文件中指定变量micoservice=false
的值来禁用其他服务常见的JWT令牌检查。
令牌发行,注册和授权控制器AuthController.java
我不会描述此控制器,因为它是完全特定的。
WebApiService.java服务
该服务在WebApiMethodProtectedController.jav
控制器中调用,并具有有趣的注释:
@PreAuthorize("hasRole('GUEST')") public Flux<User> getAllUsers() { }
此注释仅允许具有来宾角色的授权用户访问方法。
测试方法
创建环境:

获取令牌

使用收到的令牌更新环境中的TOKEN变量。
注册新用户

注册后,您将收到一个用户令牌。 它会在10小时后过期。 过期时,您需要重新购买一个。 为此,请再次请求访客令牌,更新环境并执行请求

接下来,您可以获取用户,游戏列表或创建新游戏。 还要测试Hystrix,请参阅服务配置并为git存储库加密变量。
参考文献