Перейти к основному содержимому

Spring Cloud — услуги по обеспечению безопасности

· 11 мин. чтения

1. Обзор

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

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

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

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

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

2. Настройка Мавена

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

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

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

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

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

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

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

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

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

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

eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Мы добавили базовые учетные данные для аутентификации в нашу службу обнаружения , чтобы она могла обмениваться данными со службой конфигурации . Кроме того, мы настраиваем 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:discPassword@localhost: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. Это значение дублируется в файле начальной загрузки .

Давайте зафиксируем файл в репозиторий 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 сервиса book:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/

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

management.security.sessions=never

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

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

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() из службы шлюза .

7.2. Характеристики

Добавьте эти свойства в файл bootstrap.properties в src/main/resources службы оценки:

spring.cloud.config.username=configUser
spring.cloud.config.password=configPassword
eureka.client.serviceUrl.defaultZone=
http://discUser:discPassword@localhost:8082/eureka/

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

management.security.sessions=never

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

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

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

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

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

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 .