Spring Cloud - Sécurisation des services

Spring Cloud - Sécurisation des services

1. Vue d'ensemble

Dans l'article précédent,Spring Cloud – Bootstrapping, nous avons créé une application de base pourSpring Cloud. Cet article montre comment le sécuriser.

Nous utiliserons naturellementSpring Security pour partager des sessions en utilisantSpring Session etRedis. Cette méthode est simple à configurer et à étendre à de nombreux scénarios d’entreprise. Si vous ne connaissez pasSpring Session, consultezthis article.

Le partage de sessions nous donne la possibilité de connecter les utilisateurs à notre service de passerelle et de propager cette authentification à tout autre service de notre système.

Si vous n'êtes pas familiarisé avecRedis orSpring Security, il est judicieux de passer rapidement en revue ces sujets à ce stade. Bien qu'une bonne partie de l'article soit prête à être copiée-collée pour une application, rien ne peut remplacer la compréhension de ce qui se passe sous le capot.

Pour une introduction àRedis, lisez le didacticiel dethis. Pour une introduction àSpring Security, lisezspring-security-login,role-and-privilege-for-spring-security-registration etspring-security-session. Pour avoir une compréhension complète desSpring Security,, jetez un œil auxlearn-spring-security-the-master-class.

2. Maven Setup

Commençons par ajouter la dépendancespring-boot-starter-security à chaque module du système:


    org.springframework.boot
    spring-boot-starter-security

Comme nous utilisons la gestion des dépendances deSpring, nous pouvons omettre les versions des dépendances despring-boot-starter.

Dans un second temps, modifions lespom.xml de chaque application avec les dépendancesspring-session,spring-boot-starter-data-redis:


    org.springframework.session
    spring-session


    org.springframework.boot
    spring-boot-starter-data-redis

Seules quatre de nos applications seront liées àSpring Session:discovery,gateway,book-service etrating-service.

Ensuite, ajoutez une classe de configuration de session dans les trois services dans le même répertoire que le fichier d'application principal:

@EnableRedisHttpSession
public class SessionConfig
  extends AbstractHttpSessionApplicationInitializer {
}

Enfin, ajoutez ces propriétés aux trois fichiers*.properties de notre référentiel git:

spring.redis.host=localhost
spring.redis.port=6379

Passons maintenant à la configuration spécifique au service.

3. Sécurisation du service de configuration

Le service de configuration contient des informations sensibles souvent liées aux connexions à la base de données et aux clés API. Nous ne pouvons pas compromettre ces informations, alors plongons directement et sécurisons ce service.

Ajoutons des propriétés de sécurité au fichierapplication.properties danssrc/main/resources du service de configuration:

eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/
security.user.name=configUser
security.user.password=configPassword
security.user.role=SYSTEM

Ceci configurera notre service pour vous connecter avec découverte. De plus, nous configurons notre sécurité avec le fichierapplication.properties.

Configurons maintenant notre service de découverte.

4. Sécurisation du service de découverte

Notre service de découverte contient des informations sensibles sur l'emplacement de tous les services de l'application. Il enregistre également de nouvelles instances de ces services.

Si des clients malveillants obtiennent un accès, ils apprendront l'emplacement réseau de tous les services de notre système et pourront enregistrer leurs propres services malveillants dans notre application. Il est essentiel que le service de découverte soit sécurisé.

4.1. Configuration de sécurité

Ajoutons un filtre de sécurité pour protéger les points de terminaison que les autres services utiliseront:

@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth) {
       auth.inMemoryAuthentication().withUser("discUser")
         .password("discPassword").roles("SYSTEM");
   }

   @Override
   protected void configure(HttpSecurity http) {
       http.sessionManagement()
         .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
         .and().requestMatchers().antMatchers("/eureka/**")
         .and().authorizeRequests().antMatchers("/eureka/**")
         .hasRole("SYSTEM").anyRequest().denyAll().and()
         .httpBasic().and().csrf().disable();
   }
}

Cela configurera notre service avec un utilisateur «SYSTEM». C'est une configuration de base deSpring Security avec quelques rebondissements. Jetons un coup d'œil à ces rebondissements:

  • @Order(1) - dit àSpring de câbler ce filtre de sécurité en premier afin qu'il soit tenté avant tout autre

  • .sessionCreationPolicy - dit àSpring de toujours créer une session lorsqu'un utilisateur se connecte sur ce filtre

  • .requestMatchers - limite les points finaux auxquels ce filtre s'applique

Le filtre de sécurité, que nous venons de configurer, configure un environnement d'authentification isolé se rapportant uniquement au service de découverte.

4.2. Sécurisation du tableau de bord Eureka

Étant donné que notre application de découverte dispose d'une interface utilisateur intéressante pour afficher les services actuellement enregistrés, exposons-la à l'aide d'un deuxième filtre de sécurité et associez celui-ci à l'authentification pour le reste de notre application. Gardez à l'esprit qu'aucune balise@Order() signifie qu'il s'agit du dernier filtre de sécurité à évaluer:

@Configuration
public static class AdminSecurityConfig
  extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) {
   http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
     .and().httpBasic().disable().authorizeRequests()
     .antMatchers(HttpMethod.GET, "/").hasRole("ADMIN")
     .antMatchers("/info", "/health").authenticated().anyRequest()
     .denyAll().and().csrf().disable();
   }
}

Ajoutez cette classe de configuration dans la classeSecurityConfig. Cela créera un deuxième filtre de sécurité qui contrôlera l'accès à notre interface utilisateur. Ce filtre présente quelques caractéristiques inhabituelles, examinons-les:

  • httpBasic().disable() - dit à Spring Security de désactiver toutes les procédures d'authentification pour ce filtre

  • sessionCreationPolicy - nous définissons ceci surNEVER pour indiquer que nous exigeons que l'utilisateur soit déjà authentifié avant d'accéder aux ressources protégées par ce filtre

Ce filtre ne définira jamais de session utilisateur et s'appuie surRedis pour remplir un contexte de sécurité partagé. En tant que tel, il dépend d'un autre service, la passerelle, pour fournir l'authentification.

4.3. Authentification avec le service de configuration

Dans le projet de découverte, ajoutons deux propriétés auxbootstrap.properties dans src / main / resources:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword

Ces propriétés laisseront le service de découverte s'authentifier auprès du service de configuration au démarrage.

Mettons à jour nosdiscovery.properties dans notre dépôt Git

eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Nous avons ajouté des informations d'authentification de base à notre servicediscovery pour lui permettre de communiquer avec le serviceconfig. De plus, nous configuronsEureka pour qu'il s'exécute en mode autonome en disant à notre service de ne pas s'enregistrer avec lui-même.

Commençons le fichier dans le référentielgit. Sinon, les modifications ne seront pas détectées.

5. Sécurisation du service de passerelle

Notre service de passerelle est le seul élément de notre application que nous voulons exposer au monde. En tant que tel, il aura besoin de sécurité pour garantir que seuls les utilisateurs authentifiés peuvent accéder aux informations sensibles.

5.1. Configuration de sécurité

Créons une classeSecurityConfig comme notre service de découverte et écrasons les méthodes avec ce contenu:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth.inMemoryAuthentication().withUser("user").password("password")
      .roles("USER").and().withUser("admin").password("admin")
      .roles("ADMIN");
}

@Override
protected void configure(HttpSecurity http) {
    http.authorizeRequests().antMatchers("/book-service/books")
      .permitAll().antMatchers("/eureka/**").hasRole("ADMIN")
      .anyRequest().authenticated().and().formLogin().and()
      .logout().permitAll().logoutSuccessUrl("/book-service/books")
      .permitAll().and().csrf().disable();
}

Cette configuration est assez simple. Nous déclarons un filtre de sécurité avec un identifiant de formulaire qui sécurise une variété de points de terminaison.

La sécurité sur / eureka / ** est de protéger certaines ressources statiques que nous servirons à partir de notre service de passerelle pour la page d'état deEureka. Si vous créez le projet avec l'article, copiez le dossierresource/static du projet de passerelle surGithub vers votre projet.

Nous modifions maintenant l'annotation@EnableRedisHttpSession sur notre classe de configuration:

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

Nous définissons le mode flush sur immédiat pour conserver immédiatement les modifications apportées à la session. Cela aide à préparer le jeton d'authentification pour la redirection.

Enfin, ajoutons unZuulFilter qui transmettra notre jeton d'authentification après la connexion:

@Component
public class SessionSavingZuulPreFilter
  extends ZuulFilter {

    @Autowired
    private SessionRepository repository;

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpSession httpSession = context.getRequest().getSession();
        Session session = repository.getSession(httpSession.getId());

        context.addZuulRequestHeader(
          "Cookie", "SESSION=" + httpSession.getId());
        return null;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }
}

Ce filtre saisira la demande lorsqu’il est redirigé après la connexion et ajoutera la clé de session en tant que cookie dans l’en-tête. Cela propagera l’authentification à n’importe quel service de support après la connexion.

5.2. Authentification avec le service de configuration et de découverte

Ajoutons les propriétés d'authentification suivantes au fichierbootstrap.properties danssrc/main/resources du service de passerelle:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

Ensuite, mettons à jour nosgateway.properties dans notre dépôt Git

management.security.sessions=always

zuul.routes.book-service.path=/book-service/**
zuul.routes.book-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.book-service.execution.isolation.thread
    .timeoutInMilliseconds=600000

zuul.routes.rating-service.path=/rating-service/**
zuul.routes.rating-service.sensitive-headers=Set-Cookie,Authorization
hystrix.command.rating-service.execution.isolation.thread
    .timeoutInMilliseconds=600000

zuul.routes.discovery.path=/discovery/**
zuul.routes.discovery.sensitive-headers=Set-Cookie,Authorization
zuul.routes.discovery.url=http://localhost:8082
hystrix.command.discovery.execution.isolation.thread
    .timeoutInMilliseconds=600000

Nous avons ajouté la gestion de session pour toujours générer des sessions car nous n’avons qu’un filtre de sécurité que nous pouvons définir dans le fichier de propriétés. Ensuite, nous ajoutons nos propriétés d'hôte et de serveurRedis.

De plus, nous avons ajouté un itinéraire qui redirigera les demandes vers notre service de découverte. Puisqu'un service de découverte autonome ne s'enregistrera pas avec lui-même, nous devons le localiser avec un schéma d'URL.

Nous pouvons supprimer la propriétéserviceUrl.defaultZone du fichiergateway.properties dans notre référentiel de configuration git. Cette valeur est dupliquée dans le fichierbootstrap.

Commençons le fichier dans le référentiel Git, sinon les modifications ne seront pas détectées.

6. Service de livre sécurisé

Le serveur de service de réservation contiendra des informations sensibles contrôlées par différents utilisateurs. Ce service doit être sécurisé pour éviter les fuites d'informations protégées dans notre système.

6.1. Configuration de sécurité

Pour sécuriser notre service de livre, nous copierons la classeSecurityConfig de la passerelle et remplacerons la méthode par ce contenu:

@Override
protected void configure(HttpSecurity http) {
    http.httpBasic().disable().authorizeRequests()
      .antMatchers("/books").permitAll()
      .antMatchers("/books/*").hasAnyRole("USER", "ADMIN")
      .authenticated().and().csrf().disable();
}

===

6.2. Propriétés

Ajoutez ces propriétés au fichierbootstrap.properties danssrc/main/resources du service de réservation:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

Ajoutons des propriétés à notre fichierbook-service.properties dans notre dépôt git:

management.security.sessions=never

Nous pouvons supprimer la propriétéserviceUrl.defaultZone du fichierbook-service.properties dans notre référentiel de configuration git. Cette valeur est dupliquée dans le fichierbootstrap.

N'oubliez pas de valider ces modifications pour que le service de réservation les prenne en charge.

7. Sécuriser le service de notation

Le service d'évaluation doit également être sécurisé.

7.1. Configuration de sécurité

Pour sécuriser notre service d'évaluation, nous allons copier la classeSecurityConfig de la passerelle et écraser la méthode avec ce contenu:

@Override
protected void configure(HttpSecurity http) {
    http.httpBasic().disable().authorizeRequests()
      .antMatchers("/ratings").hasRole("USER")
      .antMatchers("/ratings/all").hasAnyRole("USER", "ADMIN").anyRequest()
      .authenticated().and().csrf().disable();
}

Nous pouvons supprimer la méthodeconfigureGlobal() du servicegateway.

===

7.2. Propriétés

Ajoutez ces propriétés au fichierbootstrap.properties danssrc/main/resources du service d'évaluation:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
  http://discUser:[email protected]:8082/eureka/

Ajoutons des propriétés à notre fichier rating-service.properties dans notre référentiel git:

management.security.sessions=never

Nous pouvons supprimer la propriétéserviceUrl.defaultZone du fichier rating-service.properties dans notre référentiel de configuration git. Cette valeur est dupliquée dans le fichierbootstrap.

N'oubliez pas de valider ces modifications pour que le service d'évaluation les prenne en charge.

8. Exécution et test

DémarrezRedis et tous les services de l'application:config, discovery,gateway, book-service, etrating-service. Maintenant, testons!

Tout d'abord, créons une classe de test dans notre projetgateway et créons une méthode pour notre test:

public class GatewayApplicationLiveTest {
    @Test
    public void testAccess() {
        ...
    }
}

Ensuite, configurons notre test et validons que nous pouvons accéder à notre ressource/book-service/books non protégée en ajoutant cet extrait de code dans notre méthode de test:

TestRestTemplate testRestTemplate = new TestRestTemplate();
String testUrl = "http://localhost:8080";

ResponseEntity response = testRestTemplate
  .getForEntity(testUrl + "/book-service/books", String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

Exécutez ce test et vérifiez les résultats. Si nous constatons des échecs, confirmez que l’application entière a bien démarré et que les configurations ont été chargées à partir de notre référentiel git de configuration.

Testons maintenant que nos utilisateurs seront redirigés pour se connecter lorsqu'ils visitent une ressource protégée en tant qu'utilisateur non authentifié en ajoutant ce code à la fin de la méthode de test:

response = testRestTemplate
  .getForEntity(testUrl + "/book-service/books/1", String.class);
Assert.assertEquals(HttpStatus.FOUND, response.getStatusCode());
Assert.assertEquals("http://localhost:8080/login", response.getHeaders()
  .get("Location").get(0));

Relancez le test et confirmez qu'il a réussi.

Ensuite, connectons-nous, puis utilisons notre session pour accéder au résultat protégé par l'utilisateur:

MultiValueMap form = new LinkedMultiValueMap<>();
form.add("username", "user");
form.add("password", "password");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

maintenant, extrayons la session du cookie et propageons-la à la requête suivante:

String sessionCookie = response.getHeaders().get("Set-Cookie")
  .get(0).split(";")[0];
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
HttpEntity httpEntity = new HttpEntity<>(headers);

et demander la ressource protégée:

response = testRestTemplate.exchange(testUrl + "/book-service/books/1",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

Exécutez à nouveau le test pour confirmer les résultats.

Maintenant, essayons d'accéder à la section d'administration avec la même session:

response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());

Exécutez le test à nouveau et, comme prévu, nous ne pouvons pas accéder aux zones d'administration en tant qu'ancien utilisateur.

Le prochain test confirmera que nous pouvons nous connecter en tant qu'administrateur et accéder à la ressource protégée par l'administrateur:

form.clear();
form.add("username", "admin");
form.add("password", "admin");
response = testRestTemplate
  .postForEntity(testUrl + "/login", form, String.class);

sessionCookie = response.getHeaders().get("Set-Cookie").get(0).split(";")[0];
headers = new HttpHeaders();
headers.add("Cookie", sessionCookie);
httpEntity = new HttpEntity<>(headers);

response = testRestTemplate.exchange(testUrl + "/rating-service/ratings/all",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());
Assert.assertNotNull(response.getBody());

Notre test devient grand! Mais nous pouvons voir, lorsque nous l'exécutons, qu'en nous connectant en tant qu'administrateur, nous avons accès à la ressource admin.

Notre dernier test consiste à accéder à notre serveur de découverte via notre passerelle. Pour ce faire, ajoutez ce code à la fin de notre test:

response = testRestTemplate.exchange(testUrl + "/discovery",
  HttpMethod.GET, httpEntity, String.class);
Assert.assertEquals(HttpStatus.OK, response.getStatusCode());

Exécutez ce test une dernière fois pour vérifier que tout fonctionne. Succès!!!

Vous avez manqué ça? Parce que nous nous sommes connectés à notre service de passerelle et avons visionné le contenu de nos services de publication, d'évaluation et de découverte sans avoir à vous connecter sur quatre serveurs distincts!

En utilisantSpring Session pour propager notre objet d'authentification entre les serveurs, nous pouvons nous connecter une fois sur la passerelle et utiliser cette authentification pour accéder aux contrôleurs sur n'importe quel nombre de services de support.

9. Conclusion

La sécurité dans le cloud devient certainement plus compliquée. Mais avec l'aide deSpring Security etSpring Session, nous pouvons facilement résoudre ce problème critique.

Nous avons maintenant une application cloud avec une sécurité autour de nos services. En utilisantZuul etSpring Session, nous pouvons connecter des utilisateurs dans un seul service et propager cette authentification à l'ensemble de notre application. Cela signifie que nous pouvons facilement casser notre application dans des domaines appropriés et sécuriser chacun d’eux à notre guise.

Comme toujours, vous pouvez trouver le code source surGitHub.