مرحبا بالجميع!
في هذه المقالة ، سأوضح المكونات الأساسية لإنشاء خدمات الخلاط التفاعلي المتفاعل باستخدام Spring WebFlux ، Spring Security ، Spring Cloud Netflix Eureka (اكتشاف الخدمة) ، Hystrix (Circuit Breaker) ، Ribbon (Client Load Balancer) ، التكوين الخارجي (عبر مستودع git) ، ربيع سحابة Sleuth ، بوابة سحابة الربيع ، التمهيد الربيع رد الفعل MongoDB. وكذلك Spring Boot Admin و Zipkin للمراقبة.
تم إجراء هذه المراجعة بعد دراسة كتب Spring Microservices أثناء العمل والتدريب العملي على تطبيق Spring 5 Security للتطبيقات التفاعلية.
في هذه المقالة ، سننشئ تطبيقًا أوليًا يحتوي على ثلاثة استعلامات: الحصول على قائمة بالألعاب ، والحصول على قائمة باللاعبين ، وإنشاء لعبة من معرف اللاعبين ، وطلب التحقق من الاستعادة (Hystrix fallback) في حالة انتظار طويل للحصول على إجابة. وتنفيذ المصادقة من خلال الرمز المميز JWT استنادا إلى كتاب التدريب العملي على الأمن الربيع للتطبيقات التفاعلية.
لن أصف كيف يتم إنشاء كل تطبيق في IDE ، لأن هذه المقالة مخصصة لمستخدم ذي خبرة.
هيكل المشروع

يتكون المشروع من وحدتين. يمكن نسخ وحدة spring-servers
بأمان من مشروع إلى آخر. هناك تقريبا أي رمز وتكوينات. تحتوي وحدة tictactoe-services
على وحدات و microservices من تطبيقنا. سألاحظ على الفور أن إضافة auth-module
domain-module
إلى الخدمات ، ينتهك أحد مبادئ هندسة الخدمات المصغرة حول استقلالية الخدمات الصغيرة. لكن في مرحلة تطوير هذه الوحدات ، أعتقد أن هذا هو الحل الأفضل.
التكوين المهد
يعجبني عندما يكون تكوين 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') } }
يتيح لك استخدام ملف تكوين شائع جعل التبعيات مشتركة بين خدمات microservices ، وفي هذه الحالة ، يكون للخدمات اسم ينتهي بـ "service" ، في مكان واحد. ولكن ، هذا ينتهك مرة أخرى مبدأ استقلالية الخدمات المجهرية. بالإضافة إلى التبعيات الشائعة ، يمكنك إضافة المهام إلى المشاريع الفرعية. أضفت gradle.plugin.com.palantir.gradle.docker:gradle-docker
مهام البرنامج المساعد 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 وقائمة المسارات التي سيعالجها هذا المرشح.
فئة رد الفعل على تأمين التمهيد الربيعي 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")
تعليق توضيحي يصل هذه الوحدة بالاعتماد على متغير microservice في ملف التكوين المحدد في التعليق التوضيحي. يعد ذلك ضروريًا لتعطيل التحقق من الرمز المميز العام في بعض الوحدات النمطية. في هذا التطبيق ، هذه webapi-service
لها تطبيق خاص بها webapi-service
SecurityWebFilterChain
.
@PropertySource(value = "classpath:/application.properties")
يسمح لك هذا التعليق التوضيحي أيضًا بأخذ خصائص من الخدمة الرئيسية التي يتم استيراد هذه الوحدة إليها. بمعنى آخر ، المتغيرات
@Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls;
تأخذ قيمها من تكوين microservice من السليل.
بالإضافة إلى تعليق توضيحي يتيح لك إرفاق تعليقات توضيحية @PreAuthorize(“hasRole('MY_ROLE')”)
مثل @PreAuthorize(“hasRole('MY_ROLE')”)
@EnableReactiveMethodSecurity
وفي هذه الوحدة النمطية ، يتم إنشاء فول SecurityWebFilterChain
، الذي يقوم بتكوين الوصول إلى المشغل وعنوان url المسموح به وعنوان url الذي تم التحقق من الرمز المميز JWT عليه. تجدر الإشارة إلى أن الوصول إلى عامل تصفية الرمز المميز 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 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
والمرشح الذي يأخذ مثيل Authentication
من سياق ReactiveSecurityContext
ويقوم بإنشاء رمز مميز منه بحيث تتم مصادقة عامل التصفية بواسطة الخادم الهدف ويتم اعتماده وفقًا لذلك.
ولراحة العمل مع نوع وتواريخ 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(); }
خدمة لعبة Microservice
خدمة لعبة microservice لها البنية التالية:

كما ترون فيه ، ملف تكوين ApplicationConfig واحد فقط
Configurator 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); }
كما هو مذكور في الكتاب المذكور أعلاه ، إذا ما تم كسر شيء ما ، فدعونا نعرض البيانات المخزنة مؤقتًا أو البديلة أفضل من لا شيء.
Microservice webapi الخدمة
هذا نوع من البرامج الوسيطة بين Gateway والأجهزة الصغيرة الداخلية غير المرئية من الخارج. الغرض من هذه الخدمة هو الحصول على اختيار من خدمات أخرى وتشكيل استجابة للمستخدم على أساسها.

نبدأ الاستعراض مع التكوين.
تكوين 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);
كما نقوم بإنشاء مرشح مع هذا المدير ، ونضيف أيضًا محول مثيل المصادقة من أجل الحصول على بيانات المستخدم بترميز 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(); }
وكما كتبت أعلاه ، تعمل هذه الخدمة على تعطيل التحقق من الرمز المميز JWT الشائع للخدمات الأخرى عن طريق تحديد قيمة micoservice=false
المتغير micoservice=false
في ملف application.properites
.
إصدار الرمز ، وحدة تحكم التسجيل والتخويل AuthController.java
لن أصف وحدة التحكم هذه ، لأنها محددة تمامًا.
خدمة WebApiService.java
تسمى هذه الخدمة في WebApiMethodProtectedController.jav
تحكم ولها تعليق توضيحي مثير للاهتمام:
@PreAuthorize("hasRole('GUEST')") public Flux<User> getAllUsers() { }
يسمح هذا التعليق التوضيحي بالوصول إلى الأساليب فقط للمستخدمين المصرح لهم الذين لديهم دور الضيف.
كيفية الاختبار
خلق بيئة:

الحصول على رمزية

تحديث المتغير TOKEN في البيئة باستخدام الرمز المميز المستلم.
تسجيل مستخدم جديد

بعد التسجيل ، سوف تتلقى رمز المستخدم. تنتهي في 10 ساعات. عندما تنتهي صلاحيتها تحتاج إلى الحصول على واحدة جديدة. للقيام بذلك ، اطلب الرمز المميز للضيف مرة أخرى ، وقم بتحديث البيئة وتنفيذ الطلب

بعد ذلك ، يمكنك الحصول على قائمة المستخدمين أو الألعاب أو إنشاء لعبة جديدة. وأيضاً اختبار Hystrix ، راجع تكوينات الخدمة وتشفير المتغيرات لمستودع git.
المراجع