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
В этом сценарии любой входящий запрос, не прошедший проверку подлинности, инициирует поток кода авторизации . Как только токен получен шлюзом, он используется при отправке запросов к серверной службе:
Хорошим примером этого шаблона в действии является приложение-агрегатор каналов социальных сетей: для каждой поддерживаемой сети шлюз будет действовать как клиент OAuth 2.0.
В результате внешний интерфейс — обычно приложение SPA, созданное с помощью Angular, React или аналогичных фреймворков пользовательского интерфейса — может беспрепятственно получать доступ к данным в этих сетях от имени конечного пользователя. Что еще более важно: он может сделать это без раскрытия пользователем своих учетных данных агрегатору .
3.2. Spring Cloud Gateway как сервер ресурсов OAuth 2.0
Здесь шлюз действует как привратник, обеспечивая наличие у каждого запроса действительного токена доступа перед его отправкой серверной службе . Кроме того, он также может проверить, имеет ли токен надлежащие разрешения для доступа к данному ресурсу на основе связанных областей:
Важно отметить, что этот вид проверки разрешений в основном работает на грубом уровне. Детализированный контроль доступа (например, разрешения на уровне объекта/поля) обычно реализуется на бэкэнде с использованием доменной логики.
Одна вещь, которую следует учитывать в этом шаблоне, — это то, как серверные службы аутентифицируют и авторизуют любой перенаправленный запрос. Есть два основных случая:
Распространение токена
: 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
), мы получим экран управления областью:
Чтобы завершить настройку 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:
Поскольку мы использовали учетные данные Maxwell Smart, мы снова получаем предложение с более низкой ценой:
Чтобы завершить наш тест, мы будем использовать окно браузера анонимно/инкогнито и протестируем эту конечную точку с учетными данными Джона Сноу. На этот раз мы получаем обычную котировочную цену:
8. Заключение
В этой статье мы рассмотрели некоторые шаблоны безопасности OAuth 2.0 и способы их реализации с помощью Spring Cloud Gateway. Как обычно, весь код доступен на GitHub .