Starter to work with Spring Cloud

  • Tutorial

Hello!


In this article, I will demonstrate the basic components for creating Reactive RESTful mixservices using Spring WebFlux, Spring Security, Spring Cloud Netflix Eureka (Service Discovery), Hystrix (Circuit Breaker), Ribbon (Client Side Load Balancer), External Configuration (via git repository) , Spring Cloud Sleuth, Spring Cloud Gateway, Spring Boot Reactive MongoDB. As well as Spring Boot Admin and Zipkin for monitoring.


This review was made after studying the books of Spring Microservices in Action and the Hands-On Spring 5 Security for Reactive Applications.


In this article, we will create an elementary application with three queries: get a list of games, get a list of players, create a game from player id, a request to check for rollback (Hystrix fallback) in case of a long wait for a response. And the implementation of authentication via JWT token based on the book Hands-On Spring 5 Security for Reactive Applications.


I will not describe how each application is created in the IDE, since this article is intended for an experienced user.


Project structure


Alt text


The project consists of two modules. The module spring-serverscan be easily copied from project to project. There are almost no code and configurations. The module tictactoe-servicescontains modules and microservices of our application. Immediately, I’ll note that by adding modules to services auth-moduleand domain-moduleI’m breaking one of the principles of the microservice architecture about microservice autonomy. But at the stage of development of these modules, I believe that this is the most optimal solution.


Gradle configuration


I like it when the entire Gradle configuration is in one file, so I configured the entire project in one 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')
    }
}
// Tictactoe Modules
project(':tictactoe-services:domain-module') {
    bootJar.enabled = false
    jar {
        enabled = true
        group 'com.tictactoe'
        baseName = 'domain-module'
        version = '1.0'
    }
    dependencies {
        implementation('org.springframework.boot:spring-boot-starter-security')
        implementation('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
        implementation('org.springframework.boot:spring-boot-starter-validation')
        implementation('com.fasterxml.jackson.core:jackson-annotations:2.9.3')
        implementation 'com.intellij:annotations:+@jar'
        compileOnly('org.projectlombok:lombok')
        testCompile group: 'junit', name: 'junit', version: '4.12'
    }
}
project(':tictactoe-services:auth-module') {
    bootJar.enabled = false
    jar {
        enabled = true
        baseName = 'auth-module'
        version = '1.0'
    }
    dependencies {
        implementation project(':tictactoe-services:domain-module')implementation('org.springframework.boot:spring-boot-starter-webflux')implementation('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')implementation('org.springframework.boot:spring-boot-starter-security')implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')implementation('org.springframework.security:spring-security-oauth2-core')implementation('org.springframework.security:spring-security-oauth2-jose')
        implementation 'com.intellij:annotations:+@jar'
        testImplementation('org.springframework.boot:spring-boot-starter-test')testImplementation('io.projectreactor:reactor-test')testImplementation('org.springframework.security:spring-security-test')
    }
}
project(':tictactoe-services:user-service'){
    bootJar {
        launchScript()
        baseName = 'user-service'
        version = '0.1.0'
    }
    dependencies {
        implementation project(':tictactoe-services:domain-module')
        implementation project(':tictactoe-services:auth-module')
    }
}
project(':tictactoe-services:game-service'){
    bootJar {
        launchScript()
        baseName = 'game-service'
        version = '0.1.0'
    }
    dependencies {
        implementation project(':tictactoe-services:domain-module')
        implementation project(':tictactoe-services:auth-module')
    }
}
project(':tictactoe-services:webapi-service'){
    bootJar {
        launchScript()
        baseName = 'webapi-service'
        version = '0.1.0'
    }
    dependencies {
        implementation project(':tictactoe-services:domain-module')
        implementation project(':tictactoe-services:auth-module')
    }
}
// Spring Serversproject(':spring-servers:discovery-server'){
    bootJar {
        launchScript()
        baseName = 'discovery-server'
        version = '0.1.0'
    }
    dependencies {
        implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-server')
        implementation('org.springframework.boot:spring-boot-starter-security')
        compile('javax.xml.bind:jaxb-api:2.3.0')
        compile('javax.activation:activation:1.1')
        compile('org.glassfish.jaxb:jaxb-runtime:2.3.0')
        testImplementation('org.springframework.boot:spring-boot-starter-test')
    }
}
project(':spring-servers:config-server') {
    bootJar {
        launchScript()
        baseName = 'config-server'
        version = '0.1.0'
    }
    dependencies {
        implementation('org.springframework.boot:spring-boot-starter-security')
        implementation('org.springframework.cloud:spring-cloud-config-server')
        implementation('org.springframework.cloud:spring-cloud-starter-config')
        implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
        testImplementation('org.springframework.boot:spring-boot-starter-test')
    }
}
project(':spring-servers:gateway-server') {
    bootJar {
        launchScript()
        baseName = 'gateway-server'
        version = '0.1.0'
    }
    dependencies {
        implementation('org.springframework.boot:spring-boot-starter-webflux')
        implementation('org.springframework.boot:spring-boot-starter-actuator')
        implementation('org.springframework.cloud:spring-cloud-starter-gateway')
        implementation('org.springframework.cloud:spring-cloud-starter-config')
        implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
        implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
        testImplementation('org.springframework.boot:spring-boot-starter-test')
    }
}
project(':spring-servers:admin-server') {
    ext['springBootAdminVersion'] = '2.1.1'
    bootJar {
        launchScript()
        baseName = 'admin-server'
        version = '0.1.0'
    }
    dependencies {
        implementation('org.springframework.boot:spring-boot-starter-web')
        implementation('org.springframework.boot:spring-boot-starter-security')
        implementation('de.codecentric:spring-boot-admin-starter-server')
        implementation('org.springframework.cloud:spring-cloud-starter-config')
        implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
        testImplementation('org.springframework.boot:spring-boot-starter-test')
        testImplementation('org.springframework.security:spring-security-test')
    }
    dependencyManagement {
        imports {
            mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
        }
    }
}
subprojects { subproject ->
    if (file("${subproject.projectDir}/docker/Dockerfile").exists()) {
        docker {
            // workingbit - replace with your dockerhub's username
            name "workingbit/${subproject.group}.${subproject.bootJar.baseName}"
            tags 'latest'dockerfile file("${subproject.projectDir}/docker/Dockerfile")
            files tasks.bootJar.archivePath, 'docker/run.sh'
            buildArgs "JAR_FILE": "${subproject.bootJar.baseName}-${subproject.bootJar.version}.jar",
                    "RUN_SH": "run.sh"
        }
    } else {
        docker.name = 'noop'
    }
    if (subproject.name.endsWith('service')) {
        dependencies {
            implementation('org.springframework.boot:spring-boot-starter-actuator')
            implementation('org.springframework.boot:spring-boot-starter-webflux')
            implementation('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
            implementation('org.springframework.boot:spring-boot-starter-security')
            implementation('org.springframework.security:spring-security-oauth2-core')
            implementation('org.springframework.security:spring-security-oauth2-jose')
            implementation('org.springframework.cloud:spring-cloud-starter-config')
            implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
            implementation('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
            implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
            implementation('org.springframework.cloud:spring-cloud-starter-sleuth')
            implementation('org.springframework.cloud:spring-cloud-starter-zipkin')
            implementation('org.springframework.security:spring-security-rsa')
            implementation('com.intellij:annotations:+@jar')
            implementation('org.apache.commons:commons-lang3:3.8.1')
            runtimeOnly('org.springframework.boot:spring-boot-devtools')
            testImplementation('org.springframework.boot:spring-boot-starter-test')
            testImplementation('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
            testImplementation('io.projectreactor:reactor-test')
        }
    }
}

The use of a common configuration file allows us to place dependencies common to microservices, in this case, services with the name ending in “service” in one place. BUT, this again violates the principle of autonomy of microservices. In addition to general dependencies, you can add tasks to subprojects. I added a plugin task gradle.plugin.com.palantir.gradle.docker:gradle-dockerto work with Docker.


Auth-module


Now, consider the JWT authentication module. A description of the package of auththis module can be found in the book on reactive authentication, which I have indicated above.


Alt text


And, on the package configdwell in more detail.


The class of “complex” properties 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;
    }
}

This class contains “complex” properties for the inMemory database configuration.


Configuration class of module 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;
}

In the resource file, you must specify these variables. In my configuration, the token fades after 10 hours.


MicroserviceServiceJwtAuthWebFilter.java filter matchers configuration class


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));
    }
}

With this design, at the construction, the service for working with JWT and the list of paths that this filter will process are transferred.


Configuration class 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();
    }
}

There are three interesting annotations here.


@ConditionalOnProperty(value = "microservice", havingValue = "true")

Annotation that enables this module depending on the microservice variable in the configuration file that is specified in the annotation. This is necessary in order to disable the general token check in some modules. In this application, this is a service webapi-servicethat has its own implementation of the bean SecurityWebFilterChain.


@PropertySource(value = "classpath:/application.properties")

This annotation also allows you to take properties from the main service to which this module is imported. In other words, variables


@Value("${whiteListedAuthUrls}")
private String[] whiteListedAuthUrls;
@Value("${jwtTokenMatchUrls}")
private String[] jwtTokenMatchUrls;

Take your values ​​from the configuration of the microservice of the child.


And, the summary, which allows you to hang the annotation security type @PreAuthorize(“hasRole(‘MY_ROLE’)”)


@EnableReactiveMethodSecurity

And in this module, a bin is created SecurityWebFilterChainthat configures the access to the actuator, which are allowed by url and url on which the JWT token is checked. It should be noted that access to the JWT token filter must be open.


SpringWebFluxConfig.java configuration


In this configuration, bins are created MapReactiveUserDetailsServiceto configure the actuator and other system users in memory.


@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);
}

The bean ReactiveUserDetailsServicethat is needed to stitch our user's repository with Spring Security.


@Bean
public ReactiveUserDetailsService userDetailsRepository(UserRepository users) {
  return (email) -> users.findByEmail(email).cast(UserDetails.class);
}

Creation bean WebClient- client to perform reactive requests.


@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()));
}

Two filters are added during creation. LoadBalancerand a filter that takes an ReactiveSecurityContextinstance from the context Authenticationand creates a token from it in order for it to be authenticated by the filter of the target server and accordingly authorized.


And for the convenience of working with the MongoDB type ObjectIdand dates, I added an objectMapper creation bin:


@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 game-service


Microservice game-service has the following structure:


Alt text


As you can see there is only one ApplicationConfig configuration file.


ApplicationConfig.java Configurator


@Data
@Configuration
@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")
@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})
public class ApplicationConfig {
    @Value("${userserviceUrl}")
    private String userServiceUrl;
}

It contains a variable with the address of the service user-serviceand there are two interesting annotations:


@EnableReactiveMongoRepositories("com.tictactoe.gameservice.repository")

This annotation is necessary in order to specify the MongoDB repository to the configurator.


@Import({ApplicationClientsProperties.class, SpringWebFluxConfig.class, MicroserviceSpringSecurityWebFluxConfig.class})

This annotation imports configurations from a module auth-module.


GameService.java service


This service has only the following interesting code:


@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();
}

This method randomly throws an exception, and Hystrix, in accordance with the annotation, returns the result of the following method:


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);
}

As mentioned in the book mentioned above, if something broke, then let's better show the cached or alternative data than nothing.


Microservice webapi-service


This is a kind of middleware between Gateway and internal microservices that are not visible from the outside. The purpose of this service is to get a sample from other services and form the answer to the user on its basis.


Alt text


We will begin consideration with a configuration.


SpringSecurityWebFluxConfig.java configuration


@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
    ) {

Here we create an authentication manager for services userDetailsService, which we defined earlier in the module auth-module.


        UserDetailsRepositoryReactiveAuthenticationManager authenticationManager
                = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);

And we create a filter with this manager, and also add the Authentication instance converter to get the user data encoded in 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);
                        })
        );

Add a handler for successful authorization, the essence of which is to put the JWT token in the Authenticationrequest header generated from the request, so that you can only authenticate by valid guest token.


    tokenWebFilter.setAuthenticationSuccessHandler(new JwtAuthSuccessHandler(jwtService));
    MicroserviceServiceJwtAuthWebFilter webApiJwtServiceWebFilter = new MicroserviceServiceJwtAuthWebFilter(jwtService, jwtTokenMatchUrls);
    http.csrf().disable();
    http
            .authorizeExchange()

We allow addresses from the white list. As I wrote earlier, the addresses that will be processed by the JWT filter should also be opened


            .pathMatchers(whiteListedAuthUrls)
            .permitAll()
            .and()
            .authorizeExchange()

We protect the actuator and some addresses with basic authentication


            .pathMatchers("/actuator/**").hasRole("SYSTEM")
            .pathMatchers(HttpMethod.GET, "/url-protected/**").hasRole("GUEST")
            .pathMatchers(HttpMethod.POST, "/url-protected/**").hasRole("USER")
            .and()
            .httpBasic()
            .and()
            .authorizeExchange()

Mandatory authentication for access to the token


            .pathMatchers(AUTH_TOKEN_PATH).authenticated()
            .and()

Add filters. To authenticate and verify the JWT token.


            .addFilterAt(webApiJwtServiceWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
            .addFilterAt(tokenWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
    return http.build();
}

And as I wrote above, this service disables the JWT token check common for other services, indicating the value of the variable micoservice=falsein the file application.properites.


Token issuance, registration and authorization controller AuthController.java


I will not describe this controller, since it is very specific.


WebApiService.java service


This service is called in controller WebApiMethodProtectedController.java and has an interesting annotation:


@PreAuthorize("hasRole('GUEST')")
public Flux<User> getAllUsers() {
}

This annotation only allows access to authorized users with the guest role.


How to test


Create an environment:


Alt text


Get a token


Alt text


Update the TOKEN variable with the received token.


Register a new user


Alt text


After registration, you will receive a user token. It expires in 10 hours. When it expires you need to get a new one. To do this, request the guest token again, update the environment and make the request


Alt text


Next, you can get a list of users, games, or create a new game. And also test Hystrix, look at the configs of the services and encrypt the variables for the git repository.


Links



Also popular now: