Introduction

Vous avez lu toute la théorie sur les architectures micro services, mais vous ne savez toujours pas comment en implémenter une ? Dans ce cas cet article est probablement pour vous.

Il faut savoir qu’ont peut réaliser ça de plusieurs façons différentes, tout dépends des cas d’utilisation, technologie, plateforme cible ….

c’est pourquoi dans cet article on va essayer d’implémenter une architecture microservices basique, mais qui pourra évoluer selon vos besoins, on va utiliser principalement le language java avec le framework spring boot ainsi que différentes librairies de spring cloud.

Code source

Vous pouvez accéder au code source de cet exemple ici.

Architecture minimale

Voici l’architecture qu’on va implémenter :

Architecture de l’application

chaque brique présentée ci-dessus est un webservice à part, donc un projet spring boot par webservice.

On aura des webservices de “support” (nécéssaires au fonctionnement de l’architecture), et des webservices “métier” qui vont répondre aux besoins des cas d’utilisation métier des utilisateurs finaux.

faisons un tour des différentes briques:

Registre Service:

Chaque webservices dans notre architecture est un projet spring boot à part, donc chaque webservice aura sa propre url.

Le registre service sert à enregistrer et tracker l’ensemble de nos webservices dans leurs permettre de communiquer entre eux.

il va donc garder pour chaque service un ensemble de méta-data comme :

  • URI du service
  • nom
  • état ( up, down) …

ce registre sera sollicité par le reste des services, qu’ils soient de type “support” ou “métier”.

Passerelle:

La passerelle va jouer le rôle de reverse proxy devant l’ensemble de nos webservices, elle va donc:

  • isoler les webservices du monde extérieur: tout nos services seront accessibles via la passerelle, un utilisateur final ne devrait pas connaitre les URI des différents webservices qu’il veut utiliser, il aura juste besoin de connaître la passerelle, cette dernière va se charger de router ces requêtes aux services concernés.

  • jouer le rôle de load balancer: dans le cas ou on a plusieurs instances d’un même webservice, la passerelle va pouvoir décider à quel instance router une requête HTTP entrante selon la disponibilité de chaque instance.

La passerelle va se baser sur le registre service pour son fonctionnement.

Serveur de configuration

Ce serveur gère la configuration des différentes instances des webservices dans notre système, il va vous permettre par exemple de mettre à jour la configuration de toutes les instances d’un webservice donnée, et sans être obligé de redémarrer les instances.

Vos webservices

Les webservices à l’intérieur de notre architecture devront obligatoirement s’enregistrer dans le registre service, ne seront accessible que derrière la Passerelle et pourront optionellement charger leur configuration depuis le serveur de configuration durant la séquence de démarrage.

Gérer l’authentification et authorisation

Vous avez plein d’options pour gérer l’authentification des utilisateur et l’authorisation d’applications (clients) à accèder à votre système, dans notre exemple je vais utiliser oauth2 pour gérer l’authorisation des clients (web, mobile …) et spring security pour authentifier les utilisateurs.

un utilisateur va pouvoir s’authentifier sur notre serveur si il viens d’une application authorisée par notre serveur oauth, dans ce cas le serveur va lui donner un token qui va lui permettre d’exploiter les webservices dans notre architecture.

Exemple d’architecture

On va maintenant implémenter l’architecture suivante :

Exemple d’implémentation

on est presque dans la même architecture présentée plus haut, on ajoute un serveur d’authorisation, et 2 webservices métier:

Gestion des utilisateurs : pour gérer la création de compte utilisateurs, modification d’infos personelles …

Catalogue produit : Gestion des produits d’un magasin par exemple

Implémentation de l’exemple

Je ne vais expliquer ici que les parties les plus importantes du projet, mais vous pouvez voir l’ensemble du code sur github.

1. Registre des webservices avec Eureka

On va Commencer le registre service nommé discovery dans notre exemple, on va utiliser eureka ici, pour utiliser eureka on doit suivre les étapes suivantes :

Ajouter la dépendance dans build.gradle

dependencies {
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
}

Ajouter l’annotation @EnableEurekaServer pour activer le serveur eureka.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {

	public static void main(String[] args) {
		SpringApplication.run(DiscoveryApplication.class, args);
	}

}

configuration dans : src/main/resources/application.yml

server:
  port: 9761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    waitTimeInMsWhenSyncEmpty: 0

cette configuration va principalement dire au serveure eureka de ne pas s’enregistrer dans son propre registre, et d’écouter le port 9761.

2. Serveur de configuration

Le serveur de configuration a besoin d’une source de configurations à servir aux webservices concernés, dans notre exemple on va stocker les configurations dans un repo git.

Créez un repo git pour vos configurations ou clonez juste le repo d’exemple

git clone url-repo-example

Après cela on crée le projet configServer :

a. Dépendance dans build.gradle

dependencies {
	implementation 'org.springframework.cloud:spring-cloud-config-server'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}

Remarquez qu’on ajoute la dépendance eureka-client, le serveur de configuration sera donc enregistré dans le registre services, ainsi les autres webservices de l’application sauront ou chercher leurs configurations durant leurs phase de démarrage.

b. Configuration du serveur

server:
  port: 9889

spring:
  application:
    name: configserver
  cloud:
    config:
      server:
        git:
          uri: ${CONFIG_SERVICE_GIT_URL:file://<path to config repo>}
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: ${EUREKA_SERVICE_URL:http://localhost:9761}/eureka/

ici on dit au serveur ou se trouver les fichiers de configurations des différents webservices, puis on configure le chemin vers le registre service.

c. Ajouter les annotations pour activer le serveur de configuration

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigserverApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigserverApplication.class, args);
	}

}

on va ajouter les annotations @EnableEurekaClient et @EnableConfigServer pour activer le client eureka et le serveur de configuration.

3. Passerelle avec spring gateway

La passerelle est la porte d’entrée de notre système, elle va se baser sur l’url pour choisir à quel webservice passer la requête entrante.

on va configurer dans notre exemple les routes suivantes:

  • /api/users-management —> webservice users
  • /api/products-catalog —> webservice product
  • /oauth —> service d’authorisation et d’authentification

on va maintenant créer le projet:

a. Dépendances dans build.gradle

dependencies {
	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}

la passerelle doit utiliser le registre eureka pour trouver le chemin des webservices, on va donc utiliser encore eureka client.

on charge aussi la dépendance spring-gateway pour gérer la partie reverse proxy.

b. Configuration du serveur

spring:
  cloud:
    gateway:
      x-forwarded:
        enabled: true
        for-enabled: true
        for-append: true
        proto-enabled: false
        host-enabled: false
        port-enabled: false
      routes:
        - id: api_users_route
          uri: lb://users
          predicates:
            - Path=/api/users-management/**
        - id: api_products_route
          uri: lb://products
          predicates:
            - Path=/api/products-catalog/**
        - id: api_oauth_route
          uri: lb://oauth
          predicates:
            - Path=/oauth/**
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: ${EUREKA_SERVICE_URL:http://localhost:9761}/eureka/

c. Code Java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GatewayApplication.class, args);
	}

}

Pas besoin d’ajouter d’annotation pour la passerelle ici, la dépendance est suffisante, mais il faut quand même ajouter l’annotation EnableEurekaClient pour donner accès aux registre services.

4. Serveur d’authorisation oauth

On va gérer ici l’authentification des utilisateurs, et l’authorisation des applications (clients dans la terminologie oauth).

Ce service devra se connecter à une base de donnée postgres qui contient les utilisateurs, on va aussi utiliser spring-cloud-oauth2 pour gérer le flow oauth ainsi que security-jwt pour gérer les token jwt.

a. Dépendances dans build.gradle

dependencies {

	implementation 'org.springframework.cloud:spring-cloud-starter-security'
	implementation 'org.springframework.cloud:spring-cloud-starter-oauth2'
	implementation 'org.springframework.security:spring-security-jwt'
	implementation 'org.springframework.cloud:spring-cloud-starter-config'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

	implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
	implementation "org.springframework.boot:spring-boot-starter-data-jpa"
	implementation "org.springframework.boot:spring-boot-starter-web"

	implementation 'org.postgresql:postgresql:42.2.18'
}

b. Configuration du service

server:
  servlet:
    context-path: /oauth

health:
  config:
    enabled: false

# ===============================
# = DATABASE STUFF
# ===============================
spring:
  jpa.database-platform: org.hibernate.dialect.PostgreSQLDialect
  datasource:
    url: jdbc:postgresql://localhost:5432/mydatabase
    username: postgres
    password: password
    driverClassName: org.postgresql.Driver
    initialization-mode: always

# ===============================
# = Key to sign jwt tokens
# ===============================
signing:
  key: somekeyhere

c. sécuriser le serveur d’authorisation

On va ici sécuriser les endpoints d’oauth qui permettent de fournir des tokens pour accéder à l’application,


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;
    @Bean(name = "passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        DefaultRedirectStrategy authenticationSuccessHandlerRedirectStrategy = new DefaultRedirectStrategy();
        authenticationSuccessHandlerRedirectStrategy.setContextRelative(true);

        SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
        authenticationSuccessHandler.setDefaultTargetUrl("/");
        authenticationSuccessHandler.setAlwaysUseDefaultTargetUrl(true);
        authenticationSuccessHandler.setRedirectStrategy(authenticationSuccessHandlerRedirectStrategy);


        http.anonymous().and()
                .authorizeRequests()
                .antMatchers("/", "/public/**", "/oauth/authorize", "/authorize", "/oauth/login", "/login").permitAll()
                .and()
                .formLogin()
                .successHandler(authenticationSuccessHandler)
                .permitAll()
                .and()
                .logout()
                .permitAll().and().anonymous();
        http.csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }


}

d. Configurer le service d’authorisation

On doit configurer maintenant un client oauth, le client dans ce cas représente l’application (front) que l’utilisateur va utiliser, et c’est cette application front qui va accéder à l’api.

on peut donc dire que le token représente un utilisateur U autorisant l’application App à utiliser ces données dans l’api API.


@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends
        AuthorizationServerConfigurerAdapter {

    private TokenStore tokenStore = new InMemoryTokenStore();

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Value("${signing.key}")
    private String jwtSigningKey = "";

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        JwtAccessTokenConverter jwtAccessTokenConverter = jwtAccessTokenConverter();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));

        endpoints
                .tokenStore(this.tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(this.authenticationManager)
                .userDetailsService(userDetailsService)
                .pathMapping("/oauth/authorize", "/authorize");;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("app")
                .authorizedGrantTypes( "implicit")
                .scopes("ui-endpoints")
                .autoApprove(true)
                .redirectUris("http://localhost:9980/callback")
                .accessTokenValiditySeconds(24 * 365 * 60 * 60);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        PasswordEncoder passwordEncoder = new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence != null ? charSequence.toString() : null;
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return charSequence != null && s != null && charSequence.toString().equals(s);
            }
        };
        oauthServer.passwordEncoder(passwordEncoder);
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenStore(this.tokenStore);
        return tokenServices;
    }

    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(this.jwtSigningKey);
        return converter;
    }
}

un webservice métier

Maintenant qu’on a mis en place les webservices “techniques”, on va s’attaquer maintenant aux webservices orienté métier.

On va commencer par le webservices gestion des utilisateurs, ce webservice devra donc s’enregistrer sur le registre services, et n’accepter que les requêtes HTTP contenant un token signé par notre serveur oauth.

a. Dépendances dans build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
	implementation 'org.springframework.cloud:spring-cloud-starter-config'
	implementation 'org.springframework.cloud:spring-cloud-starter-oauth2'
	implementation 'org.springframework.security:spring-security-jwt'

	implementation "org.springframework.boot:spring-boot-starter-data-jpa"
	implementation "org.springframework.boot:spring-boot-starter-web"
	implementation 'org.liquibase:liquibase-core:3.10.3'
	implementation 'org.postgresql:postgresql:42.2.18'
}

b. Configuration du webservice

server:
  servlet:
    context-path: /api/users-management

health:
  config:
    enabled: false

# ===============================
# = DATABASE Stuff
# ===============================
spring:
  jpa.database-platform: org.hibernate.dialect.PostgreSQLDialect
  datasource:
    url: jdbc:postgresql://localhost:5432/mydatabase
    username: postgres
    password: password
    driverClassName: org.postgresql.Driver
    initialization-mode: always
  liquibase:
    change-log: classpath:/db/changelog/db.changelog-master.yaml
    enabled: true
    contexts: dev

Le webservice gestion utilisateurs va partager la base de données avec le service oauth, cependant c’est bien au niveau de la gestion utilisateurs qu’on va créer la table pour les utilisateurs.

on va gérer les modifications dans la base de données (création table, ajout de colonne …) avec liquibase.

c. Configuration niveau Java

@SpringBootApplication
@EnableEurekaClient
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UsersApplication {

	public static void main(String[] args) {
		SpringApplication.run(UsersApplication.class, args);
	}

}

l’annotation @EnableResourceServer va faire en sorte que le webservice ne soit accessible qu’avec un token.

remarquez qu’on a encore une fois utilisé l’annotation @EnableEurekaClient pour permettre au webservice de s’enregistrer sur le registre service.

Lancer le projet en local

Vous pouvez lancer l’exemple présenté dans cet article en suviant les étapes suivantes:

1. Cloner le projet

git clone git@github.com:bmarwane/microservices-project-example.git

2. Démarrer la base de données

pas la peine d’installer postgres, il suffit de lancer un container contenant notre base postgres avec la commande :

docker-compose up -d

2. Démarrer les services

la seule règle à respecter est de toujours démarrer le registre service et le service de configuration avant tout le reste, cependant pour pouvoir utiliser rapidement les services je conseil de démarrer les service selon l’ordre suivant:

  • discovery ( registre service )
  • config server
  • oauth
  • services metiers
  • gateway service

Tester le fonctionnement

j’ai ajouté un service pour jouer le rôle du serveur front end, sachant qu’on est pas obligé de servire du code front end statique de cette manière, c’est fait comme ça dans cet exemple pour faire simple.

Scenario de test

On va supposer que notre application expose une api pour explorer le catalogue de produits, pour y accéder on devra passer par notre passerelle gateway.

voici l’url cible :

http://localhost:9080/api/product-catalog/somecategory/somesubcategory

dans cet exemple la passerelle est exposée sur le port : 9080

quand on test l’api via postman (ou autre outil) le serveur nous renvoi l’erreur suivante:

Erreur renvoyée par le serveur

on constate que le serveur a besoin qu’on soit authentifié, c’est normal car on a pas ajouté de token prouvant qu’on est authentifié auprès du service d’authentification dans cette requête.

on devra donc demander un au service oauth, là aussi on devra passer par la gateway.

dans un navigateur ouvrez l’URL suivante :

http://localhost:9080/oauth/authorize?response_type=token&client_id=app&redirect_uri=http://localhost:9080/front-end/callback

utiliser les identifiants / mot de passe suivants :

login: admin
mot de passe: admin

si tout va bien vous devriez voir cette page :

Authentification réussie

On doit maintenant prendre le token généré pour le rajouter à notre requête dans postman, pour ce faire on ajoute le paramètre Authorization dans les headers de la requête, ce paramètre va contenir le token qui prouve qu’on est authentifié sur le service d’authentification.

voici le format du paramètre à rajouter :

clé: Authorization
valeur: Bearer <votre token ici>

Exemple sur postman :

Authentification réussie

Remarquez que l’api nous a bien répondu.

Recap du scénario de test

Comme on a vu dans notre exemple, on a pu accéder à une api “métier” (catalogue produits) en utilisant un token d’authentification venant du service d’authentification, et toutes nos requêtes ont circulé via la passerelle (gateway), ce qui nous a permis de cacher toute l’architecture de notre système dérière la seule url de la passerelle.

Conclusion

Implémenter une architecture micro services peut s’avérer lourd à monter et à maintenir, cette architecture ajoute une complexité par rapport à une architecture monolithique classique, cependant cette complexité viens aussi avec des avantages comme:

  • pouvoir scaller facilement voir même automatiquement certains services.
  • gestion de la configuration des services plus facile.
  • pouvoir développer et déployer chaque webservice individuellement sans impacter le reste des services.

Au vu des avantages de ce genre d’architectures, on peut constater qu’elle est surtout adaptée pour les grosses applications, c’est bien de savoir comment implémenter une architecture microservices, mais je conseil quand même d’éviter de partir sur ce genre d’archi systèmatiquement.