Halo semuanya!
Pada artikel ini, saya akan menunjukkan komponen dasar untuk membuat layanan mixer Reaktif RESTful menggunakan Spring WebFlux, Spring Security, Spring Cloud Netflix Eureka (Penemuan Layanan), Hystrix (Pemutus Sirkuit), Ribbon (Penyeimbang Beban Sisi Klien), Konfigurasi Eksternal (melalui repositori git) , Sleuth Spring Cloud, Spring Cloud Gateway, MongoDB Spring Boot Reaktif. Serta Spring Boot Admin dan Zipkin untuk pemantauan.
Ulasan ini dibuat setelah mempelajari buku-buku Spring Microservices in Action dan Hands-On Spring 5 Security untuk Aplikasi Reaktif.
Pada artikel ini, kita akan membuat aplikasi dasar dengan tiga pertanyaan: dapatkan daftar game, dapatkan daftar pemain, buat game dari id pemain, permintaan untuk memeriksa rollback (Hystrix fallback) jika menunggu lama untuk mendapat jawaban. Dan implementasi otentikasi melalui token JWT berdasarkan buku Hands-On Spring 5 Security untuk Aplikasi Reaktif.
Saya tidak akan menjelaskan bagaimana setiap aplikasi dibuat dalam IDE, karena artikel ini ditujukan untuk pengguna yang berpengalaman.
Struktur proyek

Proyek ini terdiri dari dua modul. Modul spring-servers
dapat disalin dengan aman dari proyek ke proyek. Hampir tidak ada kode dan konfigurasi. Modul tictactoe-services
berisi modul dan layanan microser dari aplikasi kita. Saya akan segera melihat bahwa menambahkan auth-module
dan auth-module
- domain-module
ke layanan, saya melanggar salah satu prinsip arsitektur layanan-mikro tentang otonomi layanan-mikro. Tetapi pada tahap pengembangan modul ini, saya percaya bahwa ini adalah solusi yang paling optimal.
Konfigurasi Gradle
Saya suka ketika seluruh konfigurasi Gradle ada dalam satu file, jadi saya mengonfigurasi seluruh proyek dalam satu 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') } }
Menggunakan file konfigurasi umum memungkinkan Anda untuk membuat dependensi umum untuk layanan microser, dalam hal ini layanan dengan nama yang berakhiran "layanan", di satu tempat. TAPI, ini lagi-lagi melanggar prinsip otonomi dari layanan-layanan mikro. Selain dependensi umum, Anda dapat menambahkan tugas ke subproyek. Saya menambahkan gradle.plugin.com.palantir.gradle.docker:gradle-docker
tugas plugin gradle.plugin.com.palantir.gradle.docker:gradle-docker
untuk bekerja dengan Docker
.
Modul otomatis
Sekarang, pertimbangkan modul otentikasi JWT. Deskripsi paket auth
dari modul ini dapat ditemukan di buku otentikasi reaktif yang saya sebutkan di atas.

Ah, config
bahas paket config
secara lebih rinci.
Kelas properti "kompleks" 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; } }
Kelas ini berisi properti "kompleks" untuk konfigurasi basis data inMemory.
Kelas konfigurasi modul 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; }
Di file sumber daya, Anda harus menentukan variabel-variabel ini. Dalam konfigurasi saya, token mati setelah 10 jam.
Kelas Konfigurasi Pencocokan Filter 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)); } }
Selama konstruksi, filter ini melewati layanan untuk bekerja dengan JWT dan daftar jalur yang akan diproses oleh filter ini.
Reactive Spring Boot Security MicroserviceSpringSecurityWebFluxConfig.java kelas konfigurasi
@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(); } }
Ada tiga penjelasan menarik di sini.
@ConditionalOnProperty(value = "microservice", havingValue = "true")
Anotasi yang menghubungkan modul ini tergantung pada variabel layanan mikro dalam file konfigurasi yang ditentukan dalam anotasi. Ini diperlukan untuk menonaktifkan pengecekan token umum di beberapa modul. Dalam aplikasi ini, ini adalah webapi-service
yang memiliki implementasi sendiri dari kacang SecurityWebFilterChain
.
@PropertySource(value = "classpath:/application.properties")
Anotasi ini juga memungkinkan Anda untuk mengambil properti dari layanan utama di mana modul ini diimpor. Dengan kata lain, variabel
@Value("${whiteListedAuthUrls}") private String[] whiteListedAuthUrls; @Value("${jwtTokenMatchUrls}") private String[] jwtTokenMatchUrls;
Ambil nilainya dari konfigurasi layanan mikro keturunan.
Dan, anotasi yang memungkinkan Anda untuk melampirkan anotasi keamanan seperti @PreAuthorize(โhasRole('MY_ROLE')โ)
@EnableReactiveMethodSecurity
Dan dalam modul ini, kacang SecurityWebFilterChain
dibuat, yang mengkonfigurasi akses ke aktuator, url dan url yang diizinkan tempat token JWT diperiksa. Perlu dicatat bahwa akses ke filter token JWT harus terbuka.
Konfigurasi SpringWebFluxConfig.java
Dalam konfigurasi ini, MapReactiveUserDetailsService
dibuat untuk mengkonfigurasi aktuator dan pengguna sistem lainnya dalam memori.
@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
yang diperlukan untuk menjahit repositori pengguna kami dengan Spring Security
.
@Bean public ReactiveUserDetailsService userDetailsRepository(UserRepository users) { return (email) -> users.findByEmail(email).cast(UserDetails.class); }
Kacang untuk membuat klien WebClient
untuk melakukan permintaan reaktif.
@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())); }
Selama pembuatan, dua filter ditambahkan. LoadBalancer
dan filter yang mengambil instance Authentication
dari konteks ReactiveSecurityContext
dan membuat token darinya sehingga filter diautentikasi oleh server target dan diotorisasi sesuai.
Dan untuk kenyamanan bekerja dengan tipe dan tanggal MongoDB ObjectId
, saya menambahkan bin kreasi 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(); }
Layanan game Microservice
Layanan permainan microservice memiliki struktur berikut:

Seperti yang Anda lihat di dalamnya, hanya satu file konfigurasi 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; }
Ini berisi variabel dengan alamat layanan user-service
dan ada dua penjelasan menarik:
@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")
Anotasi ini diperlukan untuk menunjukkan repositori MongoDB ke konfigurator.
@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})
Anotasi ini mengimpor konfigurasi dari auth-module
.
Layanan GameService.java
Layanan ini hanya memiliki kode menarik berikut:
@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(); }
Metode ini secara acak melempar pengecualian dan Hystrix sesuai dengan anotasi mengembalikan hasil dari metode berikut:
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); }
Seperti yang dinyatakan dalam buku yang disebutkan di atas, jika ada yang rusak, maka mari kita tunjukkan cache atau data alternatif lebih baik daripada tidak sama sekali.
Layanan webapi microservice
Ini adalah semacam middleware antara Gateway dan microservices internal yang tidak terlihat dari luar. Tujuan dari layanan ini adalah untuk mendapatkan seleksi dari layanan lain dan membentuk respons kepada pengguna berdasarkan pada layanan tersebut.

Kami memulai ulasan dengan konfigurasi.
Konfigurasi 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 ) {
Di sini kita membuat manajer otentikasi dari layanan userDetailsService
, yang telah kita tentukan sebelumnya dalam auth-module
.
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
Dan kami membuat filter dengan manajer ini, dan juga menambahkan konverter instance Otentikasi untuk mendapatkan data pengguna yang dikodekan dalam 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); }) );
Kami menambahkan penangan otorisasi yang sukses, intinya adalah untuk menempatkan token JWT di header permintaan yang dihasilkan dari Authentication
sehingga otentikasi hanya dapat dilakukan menggunakan token tamu yang valid.
tokenWebFilter.setAuthenticationSuccessHandler(new JwtAuthSuccessHandler(jwtService)); MicroserviceServiceJwtAuthWebFilter webApiJwtServiceWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls); http.csrf().disable(); http .authorizeExchange()
Kami menyelesaikan alamat dari daftar putih. Seperti yang saya tulis sebelumnya, alamat yang akan diproses oleh filter JWT juga harus dibuka
.pathMatchers(whiteListedAuthUrls) .permitAll() .and() .authorizeExchange()
Kami melindungi aktuator dan beberapa alamat dengan otentikasi dasar
.pathMatchers("/actuator/**").hasRole("SYSTEM") .pathMatchers(HttpMethod.GET, "/url-protected/**").hasRole("GUEST") .pathMatchers(HttpMethod.POST, "/url-protected/**").hasRole("USER") .and() .httpBasic() .and() .authorizeExchange()
Membuat akses token otentikasi wajib
.pathMatchers(AUTH_TOKEN_PATH).authenticated() .and()
Tambahkan filter. Untuk mengotentikasi dan memverifikasi token JWT.
.addFilterAt(webApiJwtServiceWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAt(tokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); }
Dan seperti yang saya tulis di atas, layanan ini menonaktifkan pemeriksaan token JWT, umum untuk layanan lain, dengan menentukan nilai variabel micoservice=false
dalam file application.properites
.
Penerbitan Token, pendaftaran, dan pengontrol otorisasi AuthController.java
Saya tidak akan menjelaskan pengontrol ini, karena ini murni spesifik.
Layanan WebApiService.java
Layanan ini dipanggil di WebApiMethodProtectedController.jav
pengontrol dan memiliki anotasi yang menarik:
@PreAuthorize("hasRole('GUEST')") public Flux<User> getAllUsers() { }
Anotasi ini memungkinkan akses ke metode hanya untuk pengguna yang berwenang dengan peran tamu.
Cara menguji
Buat lingkungan:

Dapatkan token

Perbarui variabel TOKEN di lingkungan dengan token yang diterima.
Daftarkan pengguna baru

Setelah mendaftar, Anda akan menerima token pengguna. Berakhir dalam 10 jam. Saat kedaluwarsa, Anda perlu mendapatkan yang baru. Untuk melakukan ini, minta token tamu lagi, perbarui lingkungan dan jalankan permintaan

Selanjutnya, Anda bisa mendapatkan daftar pengguna, game, atau membuat game baru. Dan juga uji Hystrix, lihat konfigurasi layanan dan mengenkripsi variabel untuk repositori git.
Referensi