«1. Введение

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

Хотя это здорово с точки зрения непрерывного развертывания и управления, оно может быстро стать запутанным, когда дело доходит до удобства использования API. Имея разные конечные точки для управления, зависимые приложения должны будут управлять CORS (совместное использование ресурсов между источниками) и разнообразным набором конечных точек.

Zuul — это пограничный сервис, который позволяет нам направлять входящие HTTP-запросы на несколько серверных микросервисов. Во-первых, это важно для предоставления унифицированного API для потребителей наших серверных ресурсов.

По сути, Zuul позволяет нам объединить все наши сервисы, сидя перед ними и выступая в качестве прокси. Он получает все запросы и направляет их в нужный сервис. Для внешнего приложения наш API выглядит как унифицированная поверхность API.

В этом руководстве мы поговорим о том, как мы можем использовать его именно для этой цели, в сочетании с OAuth 2.0 и JWT, чтобы быть на переднем крае защиты наших веб-сервисов. В частности, мы будем использовать поток предоставления пароля для получения токена доступа к защищенным ресурсам.

Быстрое, но важное замечание: мы используем поток предоставления пароля только для изучения простого сценария; большинство клиентов, скорее всего, будут использовать поток предоставления авторизации в производственных сценариях.

2. Добавление зависимостей Zuul Maven

Давайте начнем с добавления Zuul в наш проект. Мы делаем это, добавляя артефакт spring-cloud-starter-netflix-zuul: токены и сервер ресурсов, который их принимает. Эти сервисы работают на двух отдельных конечных точках.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    <version>2.0.2.RELEASE</version>
</dependency>

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

Для этого мы создадим новое приложение Spring Boot с именем GatewayApplication. Затем мы просто украсим этот класс приложения аннотацией @EnableZuulProxy, что вызовет создание экземпляра Zuul:

4. Настройка маршрутов Zuul

Прежде чем двигаться дальше, нам нужно настроить несколько свойств Zuul. Первое, что мы настроим, — это порт, на котором Zuul прослушивает входящие соединения. Это нужно сделать в файле /src/main/resources/application.yml:

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
	SpringApplication.run(GatewayApplication.class, args);
    }
}

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

Сервер авторизации развернут на: http://localhost:8081/spring-security-oauth-server/oauth

server:
    port: 8080

Сервер ресурсов развернут на: http://localhost:8082/spring-security-oauth -resource

Сервер авторизации является поставщиком удостоверений OAuth. Он существует для предоставления токенов авторизации серверу ресурсов, который, в свою очередь, предоставляет некоторые защищенные конечные точки.

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

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

На этом этапе любой запрос, поступающий в Zuul на localhost:8080/oauth/**, будет перенаправляться в службу авторизации, работающую на порту 8081. Любой запрос к localhost:8080/spring-security-oauth-resource/** будет перенаправлен на сервер ресурсов, работающий на 8082.

5. Защита внешних путей трафика Zuul

zuul:
  routes:
    spring-security-oauth-resource:
      path: /spring-security-oauth-resource/**
      url: http://localhost:8082/spring-security-oauth-resource
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth	 

Несмотря на то, что наша пограничная служба Zuul теперь маршрутизирует запросы правильно, он делает это без каких-либо проверок авторизации. Сервер авторизации, расположенный за /oauth/*, создает JWT для каждой успешной аутентификации. Естественно, он доступен анонимно.

«С другой стороны, к серверу ресурсов, расположенному по адресу /spring-security-oauth-resource/**, всегда следует обращаться с помощью JWT, чтобы гарантировать, что авторизованный клиент получает доступ к защищенным ресурсам.

Во-первых, мы настроим Zuul для прохождения через JWT к службам, которые находятся за ним. В нашем случае эти службы сами должны проверить токен.

Мы делаем это, добавляя чувствительные заголовки: Cookie,Set-Cookie.

Это завершает нашу настройку Zuul:

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

Давайте настроим Spring Security, чтобы убедиться, что авторизация проверяется в Zuul.

server:
  port: 8080
zuul:
  sensitiveHeaders: Cookie,Set-Cookie
  routes:
    spring-security-oauth-resource:
      path: /spring-security-oauth-resource/**
      url: http://localhost:8082/spring-security-oauth-resource
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth

Во-первых, нам нужно добавить в наш проект зависимости Spring Security. Нам нужны spring-security-oauth2 и spring-security-jwt:

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

Класс GatewayConfiguration определяет, как Spring Security должен обрабатывать входящие HTTP-запросы через Zuul. Внутри метода configure мы сначала сопоставили наиболее ограничительный путь с помощью antMatchers, а затем разрешили анонимный доступ через PermitAll.

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>

То есть все запросы, поступающие в /oauth/**, должны быть пропущены без проверки каких-либо токенов авторизации. Это имеет смысл, потому что это путь, по которому генерируются токены авторизации.

@Configuration
@Configuration
@EnableResourceServer
public class GatewayConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(final HttpSecurity http) throws Exception {
	http.authorizeRequests()
          .antMatchers("/oauth/**")
          .permitAll()
          .antMatchers("/**")
	  .authenticated();
    }
}

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

6. Настройка ключа, используемого для проверки JWT

Теперь, когда конфигурация настроена, все запросы, направляемые по пути /oauth/**, будут проходить анонимно, в то время как для всех остальных запросов потребуется аутентификация.

Однако здесь нам не хватает одной вещи: фактического секрета, необходимого для проверки правильности JWT. Для этого нам нужно предоставить ключ (в данном случае симметричный), используемый для подписи JWT. Вместо того, чтобы писать код конфигурации вручную, мы можем использовать spring-security-oauth2-autoconfigure.

Давайте начнем с добавления артефакта в наш проект:

Далее нам нужно добавить несколько строк конфигурации в наш файл application.yaml, чтобы определить ключ, используемый для подписи JWT:

~ ~~ Строка \»ключ-значение: 123\» задает симметричный ключ, используемый сервером авторизации для подписи JWT. Этот ключ будет использоваться spring-security-oauth2-autoconfigure для настройки синтаксического анализа токена.

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

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

security:
  oauth2:
    resource:
      jwt:
        key-value: 123

7. Тестирование пограничной службы

7.1. Получение маркера доступа

Теперь давайте проверим, как ведет себя наш пограничный сервис Zuul — с помощью нескольких команд curl.

Во-первых, мы увидим, как мы можем получить новый JWT с сервера авторизации, используя предоставление пароля.

Здесь мы обмениваем имя пользователя и пароль на токен доступа. В этом случае мы используем «john» в качестве имени пользователя и «123» в качестве пароля:

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

Обратите внимание на поле заголовка «Authorization: Basic». Он существует, чтобы сообщить серверу авторизации, какой клиент к нему подключается.

curl -X POST \
  http://localhost:8080/oauth/token \
  -H 'Authorization: Basic Zm9vQ2xpZW50SWRQYXNzd29yZDpzZWNyZXQ=' \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=password&password=123&username=john'

Для клиента (в данном случае запроса cURL) то же самое, что имя пользователя и пароль для пользователя:

7.2. Тестирование запроса к серверу ресурсов

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

{    
    "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
    "token_type":"bearer",    
    "refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
    "expires_in":3599,
    "scope":"foo read write",
    "organization":"johnwKfc",
    "jti":"8e2c56d3-3e2e-4140-b120-832783b7374b"
}

Пограничная служба Zuul теперь будет проверять JWT перед маршрутизацией к ресурсу. Сервер.

«Затем это извлекает ключевые поля из JWT и проверяет более детализированную авторизацию перед ответом на запрос:

curl -X GET \
curl -X GET \
  http:/localhost:8080/spring-security-oauth-resource/users/extra \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Accept-Encoding: gzip, deflate' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV...' \
  -H 'Cache-Control: no-cache' \

8. Многоуровневая безопасность

Важно отметить, что JWT проверяется пограничным сервисом Zuul перед передается на сервер ресурсов. Если JWT недействителен, запрос будет отклонен на границе пограничного сервиса.

{
    "user_name":"john",
    "scope":["foo","read","write"],
    "organization":"johnwKfc",
    "exp":1544584758,
    "authorities":["ROLE_USER"],
    "jti":"8e2c56d3-3e2e-4140-b120-832783b7374b",
    "client_id":"fooClientIdPassword"
}

С другой стороны, если JWT действительно действителен, запрос передается вниз по течению. Затем Resource Server снова проверяет JWT и извлекает ключевые поля, такие как область действия пользователя, организация (в данном случае настраиваемое поле) и полномочия. Он использует эти поля, чтобы решить, что пользователь может и что не может делать.

Чтобы было ясно, во многих архитектурах нам фактически не нужно проверять JWT дважды — это решение, которое вам придется принять на основе ваших шаблонов трафика.

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

9. Резюме

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

Наконец, как всегда, код доступен на Github.

«

Finally, as always, the code is available over on Github.