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

Использование Spring Cloud Gateway с шаблонами OAuth 2.0

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

1. Введение

Spring Cloud Gateway — это библиотека, которая позволяет нам быстро создавать легкие шлюзы API на основе Spring Boot, о которых мы уже рассказывали в предыдущих статьях.

На этот раз мы покажем, как быстро реализовать шаблоны OAuth 2.0 поверх него .

2. Краткий обзор OAuth 2.0

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

Хотя подробное описание этого стандарта выходит за рамки этой статьи, давайте начнем с краткого обзора нескольких ключевых терминов:

  • Ресурс : любая информация, которую могут получить только авторизованные клиенты.
  • Клиент : приложение, которое потребляет ресурсы, обычно через REST API.
  • Сервер ресурсов : служба, отвечающая за предоставление ресурса авторизованным клиентам.
  • Владелец ресурса : сущность (человек или приложение), которая владеет ресурсом и, в конечном счете, отвечает за предоставление доступа к нему клиенту.
  • Токен : фрагмент информации, полученный клиентом и отправленный на сервер ресурсов как часть запроса на его аутентификацию.
  • Поставщик удостоверений (IdP) : проверяет учетные данные пользователя и выдает токены доступа клиентам.
  • Поток аутентификации: последовательность шагов, которые должен пройти клиент, чтобы получить действительный токен.

Для исчерпывающего описания стандарта хорошей отправной точкой является документация Auth0 по этой теме .

3. Шаблоны OAuth 2.0

Spring Cloud Gateway в основном используется в одной из следующих ролей:

  • OAuth-клиент
  • Сервер ресурсов OAuth

Давайте обсудим каждый из этих случаев более подробно.

3.1. Spring Cloud Gateway как клиент OAuth 2.0

В этом сценарии любой входящий запрос, не прошедший проверку подлинности, инициирует поток кода авторизации . Как только токен получен шлюзом, он используется при отправке запросов к серверной службе:

./3f94e7926fc01eb9ff58cb3aae444ba4.png

Хорошим примером этого шаблона в действии является приложение-агрегатор каналов социальных сетей: для каждой поддерживаемой сети шлюз будет действовать как клиент OAuth 2.0.

В результате внешний интерфейс — обычно приложение SPA, созданное с помощью Angular, React или аналогичных фреймворков пользовательского интерфейса — может беспрепятственно получать доступ к данным в этих сетях от имени конечного пользователя. Что еще более важно: он может сделать это без раскрытия пользователем своих учетных данных агрегатору .

3.2. Spring Cloud Gateway как сервер ресурсов OAuth 2.0

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

./b789162ae107dac75395d28d018f0895.png

Важно отметить, что этот вид проверки разрешений в основном работает на грубом уровне. Детализированный контроль доступа (например, разрешения на уровне объекта/поля) обычно реализуется на бэкэнде с использованием доменной логики.

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

  • Распространение токена : API Gateway пересылает полученный токен серверной части как есть.
  • Замена токена : API Gateway заменяет входящий токен другим перед отправкой запроса.

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

4. Пример обзора проекта

Чтобы показать, как использовать Spring Gateway с шаблонами OAuth, которые мы описали до сих пор, давайте создадим пример проекта, который предоставляет одну конечную точку: /quotes/{symbol} . Для доступа к этой конечной точке требуется действительный маркер доступа, выданный настроенным поставщиком удостоверений.

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

Чтобы сделать вещи немного более интересными, наш серверный сервис будет возвращать разные котировки в зависимости от пользователя, связанного с запросом. Пользователи с золотой ролью получают более низкую цену, в то время как все остальные получают обычную цену (в конце концов, жизнь несправедлива ;^)).

Мы предоставим этой службе Spring Cloud Gateway, и, изменив всего несколько строк конфигурации, мы сможем переключить ее роль с клиента OAuth на сервер ресурсов.

5. Настройка проекта

5.1. Keycloak IdP

Embedded Keycloak, который мы будем использовать в этом руководстве, — это просто обычное приложение SpringBoot, которое мы можем клонировать с GitHub и собрать с помощью Maven:

$ git clone https://github.com/ForEach/spring-security-oauth
$ cd oauth-rest/oauth-authorization/server
$ mvn install

Примечание. Этот проект в настоящее время ориентирован на Java 13+, но также отлично работает с Java 11. Нам нужно только добавить -Djava.version=11 в команду Maven.

Далее мы заменим src/main/resources/foreach-domain.json на этот . Модифицированная версия имеет те же настройки, что и исходная, плюс дополнительное клиентское приложение ( quotes-client ), две группы пользователей ( golden_ и silver_customers ) и две роли ( gold и silver ).

Теперь мы можем запустить сервер с помощью подключаемого модуля spring-boot:run maven:

$ mvn spring-boot:run
... many, many log messages omitted
2022-01-16 10:23:20.318
INFO 8108 --- [ main] c.foreach.auth.AuthorizationServerApp : Started AuthorizationServerApp in 23.815 seconds (JVM running for 24.488)
2022-01-16 10:23:20.334
INFO 8108 --- [ main] c.foreach.auth.AuthorizationServerApp : Embedded Keycloak started: http://localhost:8083/auth to use keycloak

Когда сервер запущен, мы можем получить к нему доступ, указав в браузере http://localhost:8083/auth/admin/master/console/#/realms/foreach . Как только мы войдем в систему с учетными данными администратора ( bael-admin/pass ), мы получим экран управления областью:

./fd899910bebdeffed3f4421ad57142ae.png

Чтобы завершить настройку IdP, добавим пару пользователей. Первым будет Максвелл Смарт, член группы Golden_Customer . Вторым будет Джон Сноу, которого мы не будем добавлять ни в одну группу.

Используя предоставленную конфигурацию, члены группы Golden_Customers автоматически получат роль Gold .

5.2. Серверная служба

Бэкенд кавычек требует обычных зависимостей Spring Boot Reactive MVC, а также зависимости стартера сервера ресурсов :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.2</version>
</dependency>

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

В основном классе приложения мы должны включить безопасность веб-потока с помощью @EnableWebFluxSecurity :

@SpringBootApplication
@EnableWebFluxSecurity
public class QuotesApplication {
public static void main(String[] args) {
SpringApplication.run(QuotesApplication.class);
}
}

Реализация конечной точки использует предоставленный BearerAuthenticationToken , чтобы проверить, есть ли у текущего пользователя золотая роль:

@RestController
public class QuoteApi {
private static final GrantedAuthority GOLD_CUSTOMER = new SimpleGrantedAuthority("gold");

@GetMapping("/quotes/{symbol}")
public Mono<Quote> getQuote(@PathVariable("symbol") String symbol,
BearerTokenAuthentication auth ) {

Quote q = new Quote();
q.setSymbol(symbol);
if ( auth.getAuthorities().contains(GOLD_CUSTOMER)) {
q.setPrice(10.0);
}
else {
q.setPrice(12.0);
}
return Mono.just(q);
}
}

Теперь, как Spring получает роли пользователей? В конце концов, это не стандартная претензия, как области действия или электронная почта . Действительно, здесь нет никакой магии: мы должны предоставить настраиваемый ReactiveOpaqueTokenIntrospection , который извлекает эти роли из настраиваемых полей, возвращаемых Keycloak . Этот bean-компонент, доступный в Интернете, в основном такой же, как показано в документации Spring по этой теме , с небольшими изменениями, характерными для наших настраиваемых полей.

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

spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8083/auth/realms/foreach/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=quotes-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<CLIENT SECRET>

Наконец, чтобы запустить наше приложение, мы можем либо импортировать его в IDE, либо запустить из Maven. POM проекта содержит профиль для этой цели:

$ mvn spring-boot:run -Pquotes-application

Теперь приложение будет готово обслуживать запросы на http://localhost:8085/quotes . Мы можем проверить, что он отвечает, используя curl :

$ curl -v http://localhost:8085/quotes/BAEL

Как и ожидалось, мы получаем ответ 401 Unauthorized , поскольку заголовок авторизации не был отправлен.

6. Spring Gateway в качестве сервера ресурсов OAuth 2.0

Защита приложения Spring Cloud Gateway, выступающего в роли сервера ресурсов, ничем не отличается от обычной службы ресурсов. Таким образом, неудивительно, что мы должны добавить ту же начальную зависимость, что и для серверной службы:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.2</version>
</dependency>

Соответственно, мы также должны добавить @EnableWebFluxSecurity в наш класс запуска:

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

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

spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: http://localhost:8083/auth/realms/foreach/protocol/openid-connect/token/introspect
client-id: quotes-client
client-secret: <code class="language-css"><CLIENT SECRET>

Далее мы просто добавляем объявления маршрута так же, как мы делали это в нашей предыдущей статье о настройке Spring Cloud Gateway :

... other properties omitted
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**

Обратите внимание, что, кроме зависимостей и свойств безопасности, мы ничего не меняли в самом шлюзе . Чтобы запустить приложение шлюза, мы будем использовать spring-boot:run , используя определенный профиль с необходимыми настройками:

$ mvn spring-boot:run -Pgateway-as-resource-server

6.1. Тестирование сервера ресурсов

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

Далее нам нужно получить токен доступа от Keycloak. В этом случае самый простой способ получить его — использовать поток предоставления пароля (также известный как «Владелец ресурса»). Это означает выполнение POST-запроса к Keycloak с передачей имени пользователя/пароля одного из пользователей вместе с идентификатором клиента и секретом для клиентского приложения котировок:

$ curl -L -X POST \
'http://localhost:8083/auth/realms/foreach/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=quotes-client' \
--data-urlencode 'client_secret=0e082231-a70d-48e8-b8a5-fbfb743041b6' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=email roles profile' \
--data-urlencode 'username=john.snow' \
--data-urlencode 'password=1234'

Ответ будет представлять собой объект JSON, содержащий токен доступа вместе с другими значениями:

{
"access_token": "...omitted",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "...omitted",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "7fd04839-fab1-46a7-a179-a2705dab8c6b",
"scope": "profile email"
}

Теперь мы можем использовать возвращенный токен доступа для доступа к /quotes API:

$ curl --location --request GET 'http://localhost:8086/quotes/BAEL' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer xxxx...'

Что создает цитату в формате JSON:

{
"symbol":"BAEL",
"price":12.0
}

Давайте повторим этот процесс, на этот раз используя токен доступа для Maxwell Smart:

{
"symbol":"BAEL",
"price":10.0
}

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

$ curl  http://localhost:8086/quotes/BAEL

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

7. Spring Gateway как клиент OAuth 2.0

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

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

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

... other propeties omitted
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://localhost:8083/auth/realms/foreach
registration:
quotes-client:
provider: keycloak
client-id: quotes-client
client-secret: <CLIENT SECRET>
scope:
- email
- profile
- roles

Наконец, есть одно важное изменение в разделе определений маршрутов. Мы должны добавить фильтр TokenRelay к любому маршруту, который требует распространения токена доступа:

spring:
cloud:
gateway:
routes:
- id: quotes
uri: http://localhost:8085
predicates:
- Path=/quotes/**
filters:
- TokenRelay=

В качестве альтернативы, если мы хотим, чтобы все маршруты запускали поток авторизации, мы можем добавить фильтр TokenRelay в раздел фильтров по умолчанию :

spring:
cloud:
gateway:
default-filters:
- TokenRelay=
routes:
... other routes definition omitted

7.1. Тестирование Spring Gateway в качестве клиента OAuth 2.0

Для тестовой настройки нам также нужно убедиться, что у нас работают три части нашего проекта. Однако на этот раз мы запустим шлюз, используя другой профиль Spring , содержащий необходимые свойства, чтобы заставить его действовать как клиент OAuth 2.0. POM примера проекта содержит профиль, который позволяет нам запустить его с включенным этим профилем:

$ mvn spring-boot:run -Pgateway-as-oauth-client

Как только шлюз заработает, мы можем протестировать его, указав в браузере адрес http://localhost:8087/quotes/BAEL. Если все работает как положено, мы будем перенаправлены на страницу входа в IdP:

./f7dfe65d1be835ea9287eabebb05c9d2.png

Поскольку мы использовали учетные данные Maxwell Smart, мы снова получаем предложение с более низкой ценой:

./e5e77196b5f0377471cf7adf61bbeaed.png

Чтобы завершить наш тест, мы будем использовать окно браузера анонимно/инкогнито и протестируем эту конечную точку с учетными данными Джона Сноу. На этот раз мы получаем обычную котировочную цену:

./19fe5618b6d5d9e34d46d36465ea292a.png

8. Заключение

В этой статье мы рассмотрели некоторые шаблоны безопасности OAuth 2.0 и способы их реализации с помощью Spring Cloud Gateway. Как обычно, весь код доступен на GitHub .