Spring Cloud - Services sichern

1. Überblick

Im vorherigen Artikel, Spring Cloud - Bootstrapping , haben wir eine grundlegende Spring Cloud -Anwendung erstellt. Dieser Artikel zeigt, wie Sie ihn sichern können.

Natürlich verwenden wir Spring Security , um Sitzungen mit Spring Session und Redis gemeinsam zu nutzen. Diese Methode ist einfach einzurichten und lässt sich problemlos auf viele Geschäftsszenarien erweitern. Wenn Sie mit Spring Session nicht vertraut sind, überprüfen Sie den Link:/spring-session[dieser Artikel].

Durch das Teilen von Sitzungen können wir Benutzer in unserem Gateway-Dienst protokollieren und diese Authentifizierung an jeden anderen Dienst unseres Systems weiterleiten.

Wenn Sie mit Redis oder Spring Security nicht vertraut sind, empfiehlt es sich, an dieser Stelle einen kurzen Überblick über diese Themen zu erhalten. Während ein Großteil des Artikels für eine Anwendung zum Kopieren und Einfügen bereit ist, gibt es keinen Ersatz dafür, was unter der Haube passiert.

Für eine Einführung in Redis lesen Sie den Link:/spring-data-redis-tutorial[dieses]Tutorial. Für eine Einführung in Spring Security lesen Sie den Link:/spring-security-login[spring-security-login], link:/role-und-privileg für-spring-security-registration[role-und-privileg für-spring- Sicherheitsregistrierung]und Link:/spring-security-session[spring-security-session]. Um ein umfassendes Verständnis der __Spring-Sicherheit zu erlangen, besuchen Sie die learn-spring-security-the-master-class .

2. Maven-Setup

Beginnen wir mit dem Hinzufügen des https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework.boot%22%20AND%20a%3A%22spring-boot-starter-security% 22[Spring-Boot-Starter-Sicherheit]Abhängigkeit von jedem Modul im System:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Da wir das Spring -Abhängigkeitsmanagement verwenden, können wir die Versionen für spring-boot-starter -Abhängigkeiten weglassen.

Als zweiten Schritt ändern wir das pom.xml jeder Anwendung mit https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework.session%22%20AND%20a% 3A% 22spring-session% 22[Frühjahrssession], https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework.boot%22%20AND%20a%3A% 22spring-boot-starter-data-redis% 22[spring-boot-starter-data-redis]Abhängigkeiten:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Nur vier unserer Anwendungen werden in Spring Session eingebunden:

  • Discovery , Gateway , Book-Service und Rating-Service ** .

Fügen Sie als Nächstes eine Sitzungskonfigurationsklasse in allen drei Diensten in demselben Verzeichnis wie die Hauptanwendungsdatei hinzu:

@EnableRedisHttpSession
public class SessionConfig
  extends AbstractHttpSessionApplicationInitializer {
}

Fügen Sie diese Eigenschaften den drei ** . Properties -Dateien in Ihrem git-Repository hinzu:

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

Lassen Sie uns nun in die dienstspezifische Konfiguration einsteigen.

3. Sicherungsdienst sichern

Der Konfigurationsdienst enthält vertrauliche Informationen, die sich häufig auf Datenbankverbindungen und API-Schlüssel beziehen. Wir können diese Informationen nicht kompromittieren. Lassen Sie uns also direkt eintauchen und diesen Service sichern.

Fügen Sie der application.properties -Datei in src/main/resources des Konfigurationsdienstes Sicherheitseigenschaften hinzu:

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

Dadurch wird unser Dienst so eingerichtet, dass er sich mit Discovery anmeldet. Außerdem konfigurieren wir unsere Sicherheit mit der Datei application.properties .

Lassen Sie uns jetzt unseren Discovery-Dienst konfigurieren.

4. Sichern des Discovery-Service

Unser Discovery-Dienst enthält vertrauliche Informationen zum Speicherort aller Dienste in der Anwendung. Es registriert auch neue Instanzen dieser Dienste.

Wenn böswillige Clients Zugriff erhalten, erfahren sie den Netzwerkstandort aller Dienste in unserem System und können ihre eigenen bösartigen Dienste in unserer Anwendung registrieren. Es ist wichtig, dass der Discovery-Dienst gesichert ist.

4.1. Sicherheitskonfiguration

Fügen wir einen Sicherheitsfilter hinzu, um die Endpunkte zu schützen, die von den anderen Diensten verwendet werden:

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

Dadurch wird unser Dienst mit einem SYSTEM -Benutzer eingerichtet. Dies ist eine grundlegende Spring Security -Konfiguration mit einigen Drehungen. Werfen wir einen Blick auf diese Wendungen:

  • @ Order (1) - weist Spring an, diesen Sicherheitsfilter zuerst zu verdrahten

dass es vor allen anderen versucht wird ** .sessionCreationPolicy - weist Spring an, immer eine Sitzung zu erstellen

Wenn sich ein Benutzer mit diesem Filter anmeldet ** .requestMatchers - begrenzt, auf welche Endpunkte dieser Filter angewendet wird

Der Sicherheitsfilter, den wir gerade eingerichtet haben, konfiguriert eine isolierte Authentifizierungsumgebung, die nur den Erkennungsdienst betrifft.

4.2. Sichern des Eureka-Dashboards

Da unsere Discovery-Anwendung über eine schöne Benutzeroberfläche zum Anzeigen der aktuell registrierten Dienste verfügt, lassen Sie uns dies mithilfe eines zweiten Sicherheitsfilters offen legen und diesen mit der Authentifizierung für den Rest unserer Anwendung verknüpfen. Beachten Sie, dass kein @ Order () - Tag bedeutet, dass dies der letzte Sicherheitsfilter ist, der bewertet wird:

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

Fügen Sie diese Konfigurationsklasse innerhalb der Klasse SecurityConfig hinzu. Dadurch wird ein zweiter Sicherheitsfilter erstellt, der den Zugriff auf unsere Benutzeroberfläche steuert.

Dieser Filter hat einige ungewöhnliche Eigenschaften, schauen wir uns diese an:

  • httpBasic (). disable () - Weist die Spring-Sicherheit an, alle zu deaktivieren

Authentifizierungsverfahren für diesen Filter ** sessionCreationPolicy - wir setzen dies auf NEVER , um uns anzuzeigen

Der Benutzer muss bereits vor dem Zugriff auf die durch diesen Filter geschützten Ressourcen authentifiziert sein

Dieser Filter legt niemals eine Benutzersitzung fest und setzt Redis ein, um einen gemeinsam genutzten Sicherheitskontext aufzufüllen. Daher ist die Authentifizierung von einem anderen Dienst, dem Gateway, abhängig.

4.3. Authentifizierung mit dem Config-Service

Fügen Sie im Discovery-Projekt zwei Eigenschaften an bootstrap.properties in src/main/resources an:

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

Mit diesen Eigenschaften kann sich der Discovery-Dienst beim Start beim Konfigurationsdienst authentifizieren.

Lassen Sie uns discovery.properties in unserem Git-Repository aktualisieren

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

Wir haben unserem Discovery-Dienst grundlegende Anmeldeinformationen für die Authentifizierung hinzugefügt, damit er mit dem config ** -Dienst kommunizieren kann.

Außerdem konfigurieren wir Eureka so, dass es im Standalone-Modus ausgeführt wird, indem wir unseren Service anweisen, sich nicht bei sich selbst zu registrieren.

Lassen Sie uns die Datei an das git -Repository übergeben Andernfalls werden die Änderungen nicht erkannt.

5. Gateway-Service sichern

Unser Gateway-Service ist der einzige Teil unserer Anwendung, den wir der Welt vorstellen möchten. Daher ist Sicherheit erforderlich, um sicherzustellen, dass nur authentifizierte Benutzer auf vertrauliche Informationen zugreifen können.

5.1. Sicherheitskonfiguration

Erstellen Sie eine SecurityConfig -Klasse wie unseren Discovery-Service und überschreiben Sie die Methoden mit diesem Inhalt:

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

Diese Konfiguration ist ziemlich einfach. Wir deklarieren einen Sicherheitsfilter mit Formularanmeldung, der verschiedene Endpunkte sichert.

Die Sicherheit von/eureka/ dient zum Schutz einiger statischer Ressourcen, die wir von unserem Gateway-Service für die Statusseite Eureka bereitstellen. Wenn Sie das Projekt mit dem Artikel erstellen, kopieren Sie den Ordner resource/static aus dem Gateway-Projekt unter Github nach dein Projekt.

Jetzt ändern wir die Annotation @ EnableRedisHttpSession in unserer Konfigurationsklasse:

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

Wir setzen den Flush-Modus auf sofort, um Änderungen in der Sitzung sofort zu speichern. Dies hilft bei der Vorbereitung des Authentifizierungstokens für die Umleitung.

Schließlich fügen wir einen ZuulFilter hinzu, der unser Authentifizierungstoken nach der Anmeldung weiterleitet:

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

Dieser Filter greift die Anfrage auf, wenn sie nach der Anmeldung umgeleitet wird, und fügt den Sitzungsschlüssel als Cookie in die Kopfzeile ein. Dadurch wird die Authentifizierung nach der Anmeldung an einen beliebigen Backing-Service weitergegeben.

5.2. Authentifizierung mit Config und Discovery Service

Fügen Sie der Datei bootstrap.properties in src/main/resources des Gateway-Dienstes die folgenden Authentifizierungseigenschaften hinzu:

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

Als nächstes aktualisieren wir __gateway.properties__ in unserem Git-Repository

[source,text,gutter:,true]

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

Wir haben die Sitzungsverwaltung hinzugefügt, um immer Sitzungen zu generieren, da wir nur einen Sicherheitsfilter haben, den wir in der Eigenschaftendatei festlegen können.

Als Nächstes fügen wir unsere __Redis__-Host- und Servereigenschaften hinzu.

Darüber hinaus haben wir eine Route hinzugefügt, über die Anforderungen an unseren Discovery-Service weitergeleitet werden. Da sich ein eigenständiger Erkennungsdienst nicht bei sich registrieren kann, müssen Sie diesen Dienst mit einem URL-Schema ermitteln.

Wir können die __serviceUrl.defaultZone__ -Eigenschaft aus der __gateway.properties__-Datei in unserem Konfigurationsgit-Repository entfernen. Dieser Wert wird in der Datei __bootstrap__ dupliziert.

Übertragen Sie die Datei in das Git-Repository. Andernfalls werden die Änderungen nicht erkannt.

===  **  6. Buchservice sichern **

Der Buchdienstserver enthält vertrauliche Informationen, die von verschiedenen Benutzern gesteuert werden. Dieser Service muss gesichert werden, um das Auslaufen geschützter Informationen in unserem System zu verhindern.

====  **  6.1. Sicherheitskonfiguration **

Um unseren Buchservice zu sichern, kopieren wir die Klasse __SecurityConfig__ vom Gateway und überschreiben die Methode mit diesem Inhalt:

[source,java,gutter:,true]

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

====
==== **  6.2. Eigenschaften**
Fügen Sie diese Eigenschaften der Datei bootstrap.properties in hinzu
src/main/Quellen des Buchdienstes:[Quelle, Text, Rinne:, wahr]----

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

Fügen wir der __book-service.properties__-Datei in unserem git Eigenschaften hinzu
Repository:
[Quelle, Text, Rinne:, wahr]----

management.security.sessions = niemals

Wir können die serviceUrl.defaultZone -Eigenschaft aus der entfernen book-service.properties -Datei in unserem Konfigurationsgit-Repository. Diese Der Wert wird in der Datei bootstrap dupliziert.

Denken Sie daran, diese Änderungen zu übernehmen, damit der Buchservice sie abholen kann.

7. Absicherungsservice sichern

Der Rating-Service muss ebenfalls gesichert werden.

7.1. Sicherheitskonfiguration

Um unseren Rating-Service abzusichern, kopieren wir die Klasse SecurityConfig vom Gateway und überschreiben Sie die Methode mit diesem Inhalt: [Quelle, Java, Rinne:, wahr]----

@Überfahren geschützte leere Konfiguration (HttpSecurity http) {     http.httpBasic (). disable (). authorizeRequests ()       .antMatchers ("/ratings"). hasRole ("USER")       .antMatchers ("/ratings/all"). hasAnyRole ("USER", "ADMIN"). anyRequest ()       .authentifiziert () und (). csrf (). disable (); }

Wir können die Methode __configureGlobal () __ aus dem **  gateway **  -Dienst löschen.

====

====  **  7.2. Eigenschaften**

Fügen Sie diese Eigenschaften zur Datei __bootstrap.properties__ in __src/main/resources__ des Bewertungsdienstes hinzu:

[source,text,gutter:,true]

spring.cloud.config.username=configUser spring.cloud.config.password=configPassword eureka.client.serviceUrl.defaultZone= email protected :8082/eureka/----

Fügen wir der Datei rating-service.properties im git-Repository Eigenschaften hinzu:

management.security.sessions=never

Wir können die serviceUrl.defaultZone -Eigenschaft aus der Rating-Service _. Properties _ -Datei in unserem Konfigurationsgit-Repository entfernen.

Dieser Wert wird in der Datei bootstrap dupliziert.

Denken Sie daran, diese Änderungen festzuschreiben, damit der Bewertungsdienst sie abholt.

8. Laufen und Testen

Starten Sie Redis und alle Services für die Anwendung: config, discovery, gateway, book-service, und rating-service . Jetzt lass uns testen!

Zunächst erstellen wir eine Testklasse in unserem gateway -Projekt und erstellen eine Methode für unseren Test:

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

Lassen Sie uns als Nächstes unseren Test einrichten und überprüfen, dass wir auf unsere ungeschützte Ressource /book-service/books zugreifen können, indem Sie dieses Code-Snippet in unsere Testmethode einfügen:

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

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

Führen Sie diesen Test aus und überprüfen Sie die Ergebnisse. Wenn Fehler auftreten, bestätigen Sie, dass die gesamte Anwendung erfolgreich gestartet wurde und die Konfigurationen aus unserem Konfigurationsgit-Repository geladen wurden.

Lassen Sie uns nun testen, dass unsere Benutzer zur Anmeldung weitergeleitet werden, wenn Sie eine geschützte Ressource als nicht authentifizierten Benutzer besuchen, indem Sie diesen Code am Ende der Testmethode anhängen:

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

Führen Sie den Test erneut aus und bestätigen Sie, dass er erfolgreich ist.

Als Nächstes melden wir uns tatsächlich an und verwenden unsere Sitzung, um auf das vom Benutzer geschützte Ergebnis zuzugreifen:

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

Lassen Sie uns nun die Sitzung aus dem Cookie extrahieren und an die folgende Anfrage weiterleiten:

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

und fordern Sie die geschützte Ressource an:

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

Führen Sie den Test erneut aus, um die Ergebnisse zu bestätigen.

Versuchen wir nun mit derselben Sitzung auf den Admin-Bereich zuzugreifen

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

Führen Sie den Test erneut aus, und wie erwartet ist der Zugriff auf Admin-Bereiche als normaler alter Benutzer eingeschränkt.

Beim nächsten Test wird geprüft, ob wir uns als Administrator anmelden und auf die durch den Administrator geschützte Ressource zugreifen können:

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

Unser Test wird groß! Wenn wir es ausführen, können wir jedoch feststellen, dass wir durch den Login als Administrator Zugriff auf die Administratorressource erhalten.

Unser letzter Test besteht im Zugriff auf unseren Discovery-Server über unser Gateway. Fügen Sie dazu diesen Code am Ende unseres Tests hinzu:

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

Führen Sie diesen Test ein letztes Mal aus, um sicherzustellen, dass alles funktioniert.

Erfolg!!!

Hast du das vermisst? Weil wir uns bei unserem Gateway-Dienst angemeldet und Inhalte in unseren Buch-, Bewertungs- und Discovery-Diensten angesehen haben, ohne sich auf vier separaten Servern anmelden zu müssen!

Durch die Verwendung von Spring Session zur Weitergabe unseres Authentifizierungsobjekts zwischen Servern können wir uns einmalig am Gateway anmelden und diese Authentifizierung verwenden, um auf Controller mit einer beliebigen Anzahl von Backing-Services zuzugreifen.

9. Fazit

Die Sicherheit in der Cloud wird sicherlich komplizierter. Mit Hilfe von Spring Security und Spring Session können wir dieses kritische Problem jedoch leicht lösen.

Wir haben jetzt eine Cloud-Anwendung mit Sicherheit rund um unsere Services. Mit Zuul und Spring Session können wir Benutzer nur in einem Dienst protokollieren und diese Authentifizierung auf unsere gesamte Anwendung übertragen. Dies bedeutet, dass wir unsere Anwendung problemlos in ordnungsgemäße Domänen unterteilen können und jede davon nach eigenem Ermessen sichern können.

Den Quellcode finden Sie wie immer unter GitHub .