Spring Cloud - Службы безопасности

Spring Cloud - Службы безопасности

1. обзор

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

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

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

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

Чтобы познакомиться сRedis, прочтите руководство поthis. Для ознакомления сSpring Security прочтитеspring-security-login,role-and-privilege-for-spring-security-registration иspring-security-session. Чтобы получить полное представление оSpring Security,, взгляните наlearn-spring-security-the-master-class.

2. Maven Setup

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


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

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

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


    org.springframework.session
    spring-session


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

Только четыре наших приложения будут связаны сSpring Session:discovery,gateway,book-service иrating-service.

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

@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

Поскольку наше приложение для обнаружения имеет приятный пользовательский интерфейс для просмотра зарегистрированных в настоящее время служб, давайте раскроем его с помощью второго фильтра безопасности и свяжем его с аутентификацией для остальной части нашего приложения. Имейте в виду, что отсутствие тега@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 Security отключить все процедуры аутентификации для этого фильтра

  • sessionCreationPolicy - мы установили это вNEVER, чтобы указать, что мы требуем, чтобы пользователь уже прошел аутентификацию перед доступом к ресурсам, защищенным этим фильтром

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

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

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

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

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

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

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

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

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

5. Обеспечение безопасности службы шлюза

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

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

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

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

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

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

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

@EnableRedisHttpSession(
  redisFlushMode = RedisFlushMode.IMMEDIATE)

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

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

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

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

5.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/

Затем давайте обновим нашgateway.properties в нашем репозитории 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

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

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

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

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

6. Обеспечение книжного сервиса

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

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

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

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

===

6.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/

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

management.security.sessions=never

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

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

7. Обеспечение рейтингового сервиса

Рейтинговая служба также должна быть защищена.

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

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

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

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

===

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. Это значение дублируется в файлеbootstrap.

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

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

ЗапуститеRedis и все службы для приложения:config, discovery,gateway, book-service, иrating-service. А теперь попробуем!

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

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

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

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

Запустите этот тест и проверьте результаты. Если мы увидим сбои, подтвердите, что все приложение успешно запущено и что конфигурации были загружены из нашего репозитория 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 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 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.