«1. Обзор

В предыдущей статье Spring Cloud — начальная загрузка мы создали базовое приложение Spring Cloud. В этой статье показано, как его защитить.

Естественно, мы будем использовать Spring Security для совместного использования сеансов с помощью Spring Session и Redis. Этот метод прост в настройке и легко распространяется на многие бизнес-сценарии. Если вы не знакомы с Spring Session, ознакомьтесь с этой статьей.

Совместное использование сеансов дает нам возможность регистрировать пользователей в нашей службе шлюза и распространять эту аутентификацию на любую другую службу нашей системы.

Если вы не знакомы с Redis или Spring Security, рекомендуется сделать краткий обзор этих тем на этом этапе. Хотя большая часть статьи готова к копированию и вставке для приложения, нет никакой замены для понимания того, что происходит под капотом.

Для ознакомления с Redis прочитайте это руководство. Для ознакомления с Spring Security прочитайте spring-security-login, role-and-privacy-for-spring-security-registration и spring-security-session. Чтобы получить полное представление о Spring Security, ознакомьтесь с мастер-классом Learn-Spring-Security-the-Master-Class.

2. Настройка Maven

Давайте начнем с добавления зависимости spring-boot-starter-security к каждому модулю в системе:

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

Поскольку мы используем управление зависимостями Spring, мы можем опустить версии для spring- зависимости загрузчика-стартера.

В качестве второго шага давайте изменим pom.xml каждого приложения с зависимостями spring-session, spring-boot-starter-data-redis:

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

Только четыре наших приложения будут привязаны к Spring Session: открытие, шлюз, книжный сервис и рейтинговый сервис.

Затем добавьте класс конфигурации сеанса во все три службы в тот же каталог, что и основной файл приложения:

@EnableRedisHttpSession
public class SessionConfig
  extends AbstractHttpSessionApplicationInitializer {
}

Наконец, добавьте эти свойства в три файла *.properties в нашем репозитории git:

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

Теперь давайте перейдем к настройке конкретного сервиса.

3. Защита службы конфигурации

Служба конфигурации содержит конфиденциальную информацию, часто связанную с подключениями к базе данных и ключами API. Мы не можем скомпрометировать эту информацию, поэтому давайте приступим к защите этой службы.

Давайте добавим свойства безопасности в файл application.properties в src/main/resources службы конфигурации:

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

Это настроит нашу службу для входа в систему с обнаружением. Кроме того, мы настраиваем нашу безопасность с помощью файла application.properties.


Давайте теперь настроим нашу службу обнаружения.

4. Защита службы обнаружения

Наша служба обнаружения содержит конфиденциальную информацию о расположении всех служб в приложении. Он также регистрирует новые экземпляры этих служб.

Если вредоносные клиенты получат доступ, они узнают сетевое расположение всех служб в нашей системе и смогут зарегистрировать свои собственные вредоносные службы в нашем приложении. Крайне важно, чтобы служба обнаружения была защищена.

4.1. Конфигурация безопасности

Давайте добавим фильтр безопасности для защиты конечных точек, которые будут использовать другие сервисы:

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

Это установит для нашего сервиса пользователя «SYSTEM». Это базовая конфигурация Spring Security с некоторыми особенностями. Давайте взглянем на эти повороты:

    @Order(1) — говорит Spring сначала подключить этот фильтр безопасности, чтобы он выполнялся перед любыми другими .sessionCreationPolicy — говорит Spring всегда создавать сеанс, когда пользователь входит в систему с помощью этого фильтра .requestMatchers — ограничивает конечные точки, к которым применяется этот фильтр

Фильтр безопасности, который мы только что установили, настраивает изолированную среду аутентификации, относящуюся только к службе обнаружения.

4.2. Защита Eureka Dashboard

Так как наше приложение обнаружения имеет приятный пользовательский интерфейс для просмотра зарегистрированных в настоящее время сервисов, давайте представим его с помощью второго фильтра безопасности и свяжем его с аутентификацией для остальной части нашего приложения. Имейте в виду, что отсутствие тега @Order() означает, что этот фильтр безопасности будет оцениваться последним:

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

«

    «Добавьте этот класс конфигурации в класс SecurityConfig. Это создаст второй фильтр безопасности, который будет контролировать доступ к нашему пользовательскому интерфейсу. Этот фильтр имеет несколько необычных характеристик, давайте посмотрим на них:

httpBasic().disable() — говорит системе безопасности Spring отключить все процедуры аутентификации для этого фильтра. пользователь уже прошел аутентификацию перед доступом к ресурсам, защищенным этим фильтром

Этот фильтр никогда не будет устанавливать пользовательский сеанс и полагается на Redis для заполнения общего контекста безопасности. Таким образом, аутентификация зависит от другой службы, шлюза.

4.3. Аутентификация с помощью службы конфигурации

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

В проекте Discovery давайте добавим два свойства к bootstrap.properties в src/main/resources:

Эти свойства позволят службе обнаружения аутентифицироваться с помощью службы конфигурации при запуске.

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

Давайте обновим файл discovery.properties в нашем репозитории Git

Мы добавили базовые учетные данные для аутентификации в нашу службу обнаружения, чтобы она могла взаимодействовать со службой конфигурации. Кроме того, мы настраиваем Eureka для работы в автономном режиме, говоря нашему сервису не регистрироваться у себя.

Давайте закоммитим файл в репозиторий git. В противном случае изменения не будут обнаружены.

5. Защита службы шлюза

Наша служба шлюза — это единственная часть нашего приложения, которую мы хотим открыть миру. Таким образом, ему потребуется безопасность, чтобы гарантировать, что только аутентифицированные пользователи могут получить доступ к конфиденциальной информации.

5.1. Конфигурация безопасности


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

Давайте создадим класс SecurityConfig, подобный нашей службе обнаружения, и перезапишем методы следующим содержимым:

Эта конфигурация довольно проста. Мы объявляем фильтр безопасности с формой входа в систему, который защищает различные конечные точки.

Безопасность в /eureka/** предназначена для защиты некоторых статических ресурсов, которые мы будем обслуживать из нашей службы шлюза для страницы состояния Eureka. Если вы создаете проект со статьей, скопируйте папку resource/static из проекта шлюза на Github в свой проект.

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

Теперь мы изменим аннотацию @EnableRedisHttpSession в нашем классе конфигурации:

Мы устанавливаем режим сброса на немедленное, чтобы немедленно сохранять любые изменения в сеансе. Это помогает подготовить токен аутентификации для перенаправления.

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

Наконец, давайте добавим ZuulFilter, который будет пересылать наш токен аутентификации после входа в систему:

Этот фильтр будет захватывать запрос, поскольку он перенаправляется после входа в систему, и добавлять сеансовый ключ в качестве файла cookie в заголовок. Это распространит аутентификацию на любую вспомогательную службу после входа в систему.

5.2. Аутентификация с помощью службы конфигурации и обнаружения

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

Давайте добавим следующие свойства аутентификации в файл bootstrap.properties в src/main/resources службы шлюза:


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

Далее давайте обновим наши gateway.properties в нашем Git репозиторий

Мы добавили управление сеансами, чтобы всегда создавать сеансы, потому что у нас есть только один фильтр безопасности, который мы можем установить в файле свойств. Затем мы добавляем свойства хоста и сервера Redis.

Кроме того, мы добавили маршрут, который будет перенаправлять запросы в наш сервис обнаружения. Поскольку автономная служба обнаружения не будет регистрироваться сама по себе, мы должны найти эту службу с помощью схемы URL.

Мы можем удалить свойство serviceUrl.defaultZone из файла gateway.properties в нашем git-репозитории конфигурации. Это значение дублируется в файле начальной загрузки.

Давайте зафиксируем файл в репозиторий Git, иначе изменения не будут обнаружены.

6. Защита книжной службы

Сервер книжной службы будет хранить конфиденциальную информацию, контролируемую различными пользователями. Эта служба должна быть защищена, чтобы предотвратить утечку защищенной информации в нашу систему.

6.1. Конфигурация безопасности

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

Для защиты нашего книжного сервиса мы скопируем класс SecurityConfig со шлюза и перезапишем метод следующим содержанием:

6.2. Свойства

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

«Добавьте эти свойства в файл bootstrap.properties в src/main/resources сервиса book:


management.security.sessions=never

Давайте добавим свойства в наш файл book-service.properties в нашем репозитории git:

Мы можем удалите свойство serviceUrl.defaultZone из файла book-service.properties в нашем git-репозитории конфигурации. Это значение дублируется в файле начальной загрузки.

Не забудьте зафиксировать эти изменения, чтобы книжный сервис их подхватил.

7. Защита службы оценки

Служба оценки также должна быть защищена.

7.1. Конфигурация безопасности

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

Чтобы обезопасить нашу службу оценки, мы скопируем класс SecurityConfig из шлюза и перезапишем метод следующим содержимым:

Мы можем удалить метод configureGlobal() из службы шлюза.

7.2. Свойства

Добавьте эти свойства в файл bootstrap.properties в src/main/resources сервиса рейтинга:

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

Давайте добавим свойства в наш файл rating-service.properties в нашем репозитории git:

management.security.sessions=never

~ ~~ Мы можем удалить свойство serviceUrl.defaultZone из файла rating-service.properties в нашем git-репозитории конфигурации. Это значение дублируется в файле начальной загрузки.


Не забудьте зафиксировать эти изменения, чтобы рейтинговая служба их зафиксировала.

8. Запуск и тестирование

Запустите Redis и все службы приложения: конфигурацию, обнаружение, шлюз, книжную службу и рейтинговую службу. Теперь давайте тестировать!

Во-первых, давайте создадим тестовый класс в нашем проекте шлюза и создадим метод для нашего теста:

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

Затем давайте настроим наш тест и подтвердим, что мы можем получить доступ к нашему незащищенному ресурсу /book-service/books. добавив этот фрагмент кода в наш тестовый метод:

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

Запустите этот тест и проверьте результаты. Если мы видим сбои, подтвердите, что все приложение успешно запущено и что конфигурации были загружены из нашего репозитория конфигурации git.

Теперь давайте проверим, что наши пользователи будут перенаправлены для входа в систему при посещении защищенного ресурса в качестве пользователя, не прошедшего проверку подлинности, добавив этот код в конец метода тестирования:

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

Запустите тест еще раз и убедитесь, что он удается.

Далее, давайте действительно войдем в систему, а затем используем нашу сессию для доступа к результату, защищенному пользователем:

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

теперь давайте извлечем сессию из файла cookie и распространим ее на следующий запрос:

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

~~ ~ и запросите защищенный ресурс:

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

Запустите тест еще раз, чтобы подтвердить результаты.

Теперь давайте попробуем получить доступ к разделу администратора с той же сессией:

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

Запустите тест еще раз, и, как и ожидалось, мы не сможем получить доступ к разделу администратора как обычный старый пользователь.

Следующий тест подтвердит, что мы можем войти в систему как администратор и получить доступ к защищенному администратором ресурсу:

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

Наш тест становится большим! Но мы можем видеть, когда мы запускаем его, что, войдя в систему как администратор, мы получаем доступ к ресурсу администратора.

Наш последний тест — доступ к нашему серверу обнаружения через наш шлюз. Для этого добавьте этот код в конец нашего теста:

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

Запустите этот тест в последний раз, чтобы убедиться, что все работает. Успех!!!

Вы пропустили это? Потому что мы вошли в нашу службу шлюза и просматривали контент в наших книгах, рейтингах и службах обнаружения без необходимости входа в систему на четырех отдельных серверах!

Используя Spring Session для распространения нашего объекта аутентификации между серверами, мы можем один раз войти в систему на шлюзе и использовать эту аутентификацию для доступа к контроллерам на любом количестве резервных служб.

9. Заключение

Безопасность в облаке, безусловно, усложняется. Но с помощью Spring Security и Spring Session мы можем легко решить эту критическую проблему.

Теперь у нас есть облачное приложение с защитой наших сервисов. Используя Zuul и Spring Session, мы можем регистрировать пользователей только в одной службе и распространять эту аутентификацию на все наше приложение. Это означает, что мы можем легко разбить наше приложение на соответствующие домены и защитить каждый из них по своему усмотрению.

Как всегда, вы можете найти исходный код на GitHub.