Рубрики
Без рубрики

Весенние облачные службы безопасности

В статье объясняются проблемы обеспечения безопасности облачных сервисов Spring и способы их решения.

Автор оригинала: Tim Schimandle.

1. Обзор

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

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

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

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

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

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

Давайте начнем с добавления зависимости 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

Только четыре наших приложения будут подключены к весенней сессии : 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

Это настроит нашу службу для входа в систему с помощью discovery. Кроме того, мы настраиваем нашу безопасность с помощью файла 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 – мы устанавливаем это значение в НИКОГДА , чтобы указать, что мы требуем, чтобы пользователь уже прошел проверку подлинности до доступа к ресурсам, защищенным этим фильтром

Этот фильтр никогда не будет устанавливать сеанс пользователя и полагается на 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. Конфигурация безопасности

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

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

Мы можем удалить метод configure Global() из службы 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/

Давайте добавим свойства в наш рейтинг-сервис .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() {
        ...
    }
}

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

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

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