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>
3. Включение Зуула
Приложение, которое мы хотим направить через Zuul, содержит сервер авторизации OAuth 2.0, который предоставляет токены доступа, и сервер ресурсов, который их принимает. Эти сервисы работают на двух отдельных конечных точках.
Мы хотели бы иметь единую конечную точку для всех внешних клиентов этих служб с разными путями, ведущими к разным физическим конечным точкам. Для этого мы представим Zuul в качестве пограничного сервиса.
Для этого мы создадим новое приложение Spring Boot с именем GatewayApplication
. Затем мы просто украсим этот класс приложения аннотацией @EnableZuulProxy
, которая вызовет создание экземпляра Zuul:
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4. Настройка маршрутов Zuul
Прежде чем мы сможем пойти дальше, нам нужно настроить несколько свойств Zuul. Первое, что мы настроим, — это порт, на котором Zuul прослушивает входящие соединения. Это нужно сделать в файле /src/main/resources/application.yml
:
server:
port: 8080
Теперь самое интересное — настройка фактических маршрутов, на которые Zuul будет перенаправлять. Для этого нам нужно отметить следующие службы, их пути и порты, которые они прослушивают.
Сервер авторизации развернут на: http://localhost:8081/spring-security-oauth-server/oauth
Сервер ресурсов развернут на: http://localhost:8082/spring-security-oauth-resource.
Сервер авторизации является поставщиком удостоверений OAuth. Он существует для предоставления токенов авторизации серверу ресурсов, который, в свою очередь, предоставляет некоторые защищенные конечные точки.
Сервер авторизации предоставляет токен доступа клиенту, который затем использует этот токен для выполнения запросов к серверу ресурсов от имени владельца ресурса. Краткий обзор терминологии OAuth поможет нам держать эти концепции в поле зрения.
Теперь давайте сопоставим некоторые маршруты с каждым из этих сервисов:
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 на localhost:8080/oauth/**
, будет перенаправлен в службу авторизации, работающую на порту 8081. Любой запрос на localhost:8080/spring-security-oauth-resource/**
будет перенаправлен на сервер ресурсов работает на 8082.
5. Защита путей внешнего трафика Zuul
Несмотря на то, что наша пограничная служба Zuul теперь правильно маршрутизирует запросы, она делает это без каких-либо проверок авторизации. Сервер авторизации, расположенный за /oauth/*
, создает JWT для каждой успешной аутентификации. Естественно, он доступен анонимно.
С другой стороны, к серверу ресурсов, расположенному по адресу /spring-security-oauth-resource/**
, всегда следует обращаться с помощью JWT, чтобы гарантировать, что авторизованный клиент получает доступ к защищенным ресурсам.
Во-первых, мы настроим Zuul для прохождения через JWT к службам, которые находятся за ним. В нашем случае эти службы сами должны проверить токен.
Мы делаем это, добавляя чувствительные заголовки: Cookie,Set-Cookie
.
На этом наша конфигурация 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
После того, как мы избавились от этого, нам нужно разобраться с авторизацией на границе. Прямо сейчас Zuul не будет проверять JWT перед передачей его нижестоящим службам. Эти службы сами будут проверять JWT, но в идеале мы хотели бы, чтобы пограничная служба делала это первой и отклоняла любые несанкционированные запросы, прежде чем они проникнут глубже в нашу архитектуру.
Давайте настроим Spring Security, чтобы убедиться, что авторизация проверяется в Zuul.
Во-первых, нам нужно добавить в наш проект зависимости Spring Security. Нам нужны spring-security-oauth2 и spring-security-jwt:
<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>
Теперь давайте напишем конфигурацию для маршрутов, которые мы хотим защитить, расширив ResourceServerConfigurerAdapter:
@Configuration
@Configuration
@EnableResourceServer
public class GatewayConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**")
.permitAll()
.antMatchers("/**")
.authenticated();
}
}
Класс GatewayConfiguration
определяет, как Spring Security должен обрабатывать входящие HTTP-запросы через Zuul. Внутри метода configure
мы сначала сопоставили наиболее ограничительный путь с помощью antMatchers,
а затем разрешили анонимный доступ с помощью allowAll
.
То есть все запросы, поступающие в /oauth/**
, должны быть пропущены без проверки каких-либо токенов авторизации. Это имеет смысл, потому что это путь, по которому генерируются токены авторизации.
Затем мы сопоставили все остальные пути с /**
и через вызов authentication
настаивали на том, чтобы все остальные вызовы содержали токены доступа.
6. Настройка ключа, используемого для проверки JWT
Теперь, когда конфигурация настроена, все запросы, направляемые по пути /oauth/**
, будут проходить анонимно, в то время как для всех остальных запросов потребуется аутентификация.
Однако здесь нам не хватает одной вещи, а именно фактического секрета, необходимого для проверки правильности JWT. Для этого нам нужно предоставить ключ (в данном случае симметричный), используемый для подписи 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>
Затем нам нужно добавить несколько строк конфигурации в наш файл application.yaml
, чтобы определить ключ, используемый для подписи JWT:
security:
oauth2:
resource:
jwt:
key-value: 123
Строка " ключ-значение: 123
" задает симметричный ключ, используемый сервером авторизации для подписи JWT. Этот ключ будет использоваться spring-security-oauth2-autoconfigure
для настройки синтаксического анализа токена.
Важно отметить, что в производственной системе не следует использовать симметричный ключ, указанный в исходном коде приложения. Это, естественно, должно быть настроено извне.
7. Тестирование пограничной службы
7.1. Получение токена доступа
Теперь давайте проверим, как ведет себя наш пограничный сервис Zuul, с помощью нескольких команд curl.
Во-первых, мы увидим, как мы можем получить новый JWT с сервера авторизации, используя предоставление пароля .
Здесь мы обмениваем имя пользователя и пароль на токен доступа . В этом случае мы используем « john
» в качестве имени пользователя и « 123
» в качестве пароля:
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'
Этот вызов дает токен JWT, который мы затем можем использовать для аутентифицированных запросов к нашему серверу ресурсов.
Обратите внимание на поле заголовка «Авторизация: базовая…» .
Он существует, чтобы сообщить серверу авторизации, какой клиент к нему подключается.
Это для клиента (в данном случае запроса cURL), что имя пользователя и пароль для пользователя:
{
"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
"token_type":"bearer",
"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX...",
"expires_in":3599,
"scope":"foo read write",
"organization":"johnwKfc",
"jti":"8e2c56d3-3e2e-4140-b120-832783b7374b"
}
7.2. Тестирование запроса сервера ресурсов
Затем мы можем использовать 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' \
Пограничная служба Zuul теперь будет проверять JWT перед маршрутизацией на сервер ресурсов.
Затем это извлекает ключевые поля из JWT и проверяет более детальную авторизацию перед ответом на запрос:
{
"user_name":"john",
"scope":["foo","read","write"],
"organization":"johnwKfc",
"exp":1544584758,
"authorities":["ROLE_USER"],
"jti":"8e2c56d3-3e2e-4140-b120-832783b7374b",
"client_id":"fooClientIdPassword"
}
8. Безопасность на разных уровнях
Важно отметить, что JWT проверяется пограничной службой Zuul перед передачей на сервер ресурсов. Если JWT недействителен, запрос будет отклонен на границе пограничного сервиса.
С другой стороны, если JWT действительно действителен, запрос передается вниз по течению. Затем Resource Server снова проверяет JWT и извлекает ключевые поля, такие как область действия пользователя, организация (в данном случае настраиваемое поле) и полномочия. Он использует эти поля, чтобы решить, что пользователь может и не может делать.
Чтобы было ясно, во многих архитектурах нам фактически не нужно дважды проверять JWT — это решение, которое вам придется принять на основе ваших шаблонов трафика.
Например, в некоторых производственных проектах к отдельным серверам ресурсов можно обращаться напрямую, а также через прокси-сервер, и нам может потребоваться проверить токен в обоих местах. В других проектах трафик может идти только через прокси, в этом случае достаточно проверить токен там.
9. Резюме
Как мы видели, Zuul предоставляет простой, настраиваемый способ абстрагирования и определения маршрутов для сервисов. Вместе с Spring Security это позволяет нам авторизовать запросы на границах службы.
Наконец, как всегда, код доступен на Github .