Рубрики
Без рубрики

Поддержка веб-клиента Spring и OAuth2

Узнайте, как настроить приложение в качестве клиента OAuth2 и использовать WebClient для получения защищенного ресурса в полнореактивном стеке.

Автор оригинала: Ger Roza.

1. Обзор

Spring Security 5 обеспечивает поддержку OAuth2 для неблокирующего класса Spring Webflux WebClient .

В этом уроке мы проанализируем различные подходы к доступу к защищенным ресурсам с помощью этого класса.

Кроме того, мы заглянем под капот, чтобы понять, как Spring обрабатывает процесс авторизации OAuth2.

2. Настройка сценария

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

Мы можем использовать известных поставщиков авторизации, таких как Google или Github. Чтобы лучше понять роль клиента OAuth2, мы также можем использовать наши собственные серверы, с реализацией, доступной здесь . Мы не будем показывать полную конфигурацию, так как это не тема этого урока, достаточно знать, что:

  • сервер авторизации будет:
    • работает на порту 8081
    • предоставление /oauth/authorize, oauth/token/| и oauth| check_token/| конечным точкам для выполнения требуемой функциональности настроено с образцами пользователей (например, john
    • / 123 ) и один клиент OAuth ( foo ClientID Password /secret ) сервер ресурсов будет отделен от Сервера аутентификации и будет: работает на порту
  • 8082
    • обслуживание простого Foo
    • защищенного ресурса объекта, доступного с помощью /foos/{id} конечной точки

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

WebClient и все функции, связанные с реактивным Webflux, являются частью проекта Spring Security 5. Поэтому мы будем в основном использовать эту структуру на протяжении всей этой статьи.

3. Пружинная защита 5 Под капотом

Чтобы полностью понять примеры, которые будут приведены ниже, полезно знать, как Spring Security управляет функциями OAuth2 внутри.

Эта структура предлагает возможности для:

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

Некоторые из фундаментальных концепций мира OAuth2 Spring Security описаны на следующей диаграмме:

3.1. Поставщики услуг

Spring определяет роль поставщика OAuth2, ответственного за предоставление защищенных ресурсов OAuth 2.0.

В нашем примере наша служба аутентификации будет предлагать возможности Поставщика.

3.2. Регистрация Клиентов

A Регистрация клиента – это объект, содержащий всю соответствующую информацию о конкретном клиенте, зарегистрированном в поставщике OAuth2 (или OpenID).

В нашем сценарии это будет клиент, зарегистрированный на сервере аутентификации, идентифицированный с помощью bael-client-id id.

3.3. Авторизованные Клиенты

Как только конечный пользователь (он же Владелец ресурса) предоставляет клиенту разрешения на доступ к его ресурсам, OAuth2AuthorizedClient создается сущность.

Он будет отвечать за связывание токенов доступа с регистрациями клиентов и владельцами ресурсов (представленными объектами Principal ).

3.4. Репозитории

Кроме того, Spring Security также предлагает классы репозиториев для доступа к объектам, упомянутым выше.

В частности, в реактивных стеках используются классы Репозиторий регистрации реактивного клиента и Репозиторий авторизованного клиента сервера OAuth2 , и по умолчанию они используют хранилище в памяти.

Spring Boot 2.x создает компоненты этих классов репозитория и автоматически добавляет их в контекст.

3.5. Цепочка Веб-фильтров Безопасности

Одним из ключевых понятий в Spring Security 5 является цепочка веб-фильтров reactive Security entity.

Как следует из его названия, он представляет собой цепочку объектов веб-фильтра.

Когда мы включаем функции OAuth2 в нашем приложении, Spring Security добавляет в цепочку два фильтра:

  1. Один фильтр отвечает на запросы авторизации ( /oauth2/authorization/{идентификатор регистрации} URI) или выдает Исключение ClientAuthorizationRequiredException . Он содержит ссылку на репозиторий регистрации Реактивного клиента, и отвечает за создание запроса авторизации для перенаправления агента пользователя.
  2. Второй фильтр отличается в зависимости от того, какую функцию мы добавляем (возможности клиента OAuth2 или функциональность входа в систему OAuth2). В обоих случаях основная ответственность этого фильтра заключается в создании OAuth2 Авторизованного клиента экземпляра и хранении его с помощью ServerOAuth2AuthorizedClientRepository.

3.6. Веб-клиент

Веб-клиент будет настроен с функцией фильтра Exchange , содержащей ссылки на репозитории.

Он будет использовать их для получения маркера доступа, чтобы автоматически добавить его в запрос.

4. Поддержка Spring Security 5 – Поток учетных данных клиента

Spring Security позволяет настроить наше приложение в качестве клиента OAuth2.

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

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

4.1. Конфигурации Клиента и Поставщика

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

spring.security.oauth2.client.registration.bael.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret

spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token

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

4.2. Использование веб-клиента

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

Например, давайте представим, что у нас есть задание cron , пытающееся получить защищенный ресурс с помощью WebClient в нашем приложении:

@Autowired
private WebClient webClient;

@Scheduled(fixedRate = 5000)
public void logResourceServiceResponse() {

    webClient.get()
      .uri("http://localhost:8084/retrieve-resource")
      .retrieve()
      .bodyToMono(String.class)
      .map(string 
        -> "Retrieved using Client Credentials Grant Type: " + string)
      .subscribe(logger::info);
}

4.3. Настройка Веб – клиента

Далее, давайте установим экземпляр WebClient , который мы автоматически подключили в нашей запланированной задаче:

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    oauth.setDefaultClientRegistrationId("bael");
    return WebClient.builder()
      .filter(oauth)
      .build();
}

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

Следующее, что следует отметить здесь, это то, что мы используем Не прошедший проверку подлинности Сервер OAuth2 Авторизованный Клиентский репозиторий экземпляр. Это связано с тем, что ни один конечный пользователь не будет участвовать в процессе, поскольку это связь между машинами. Наконец, мы заявили, что по умолчанию будем использовать регистрацию клиента bael .

В противном случае нам пришлось бы указать его к моменту определения запроса в задании cron:

webClient.get()
  .uri("http://localhost:8084/retrieve-resource")
  .attributes(
    ServerOAuth2AuthorizedClientExchangeFilterFunction
      .clientRegistrationId("bael"))
  .retrieve()
  // ...

4.4. Тестирование

Если мы запустим наше приложение с включенным уровнем DEBUG logging, мы сможем увидеть вызовы, которые Spring Security выполняет для нас:

o.s.w.r.f.client.ExchangeFunctions:
  HTTP POST http://localhost:8085/oauth/token
o.s.http.codec.json.Jackson2JsonDecoder:
  Decoded [{access_token=89cf72cd-183e-48a8-9d08-661584db4310,
    token_type=bearer,
    expires_in=41196,
    scope=read
    (truncated)...]
o.s.w.r.f.client.ExchangeFunctions:
  HTTP GET http://localhost:8084/retrieve-resource
o.s.core.codec.StringDecoder:
  Decoded "This is the resource!"
c.b.w.c.service.WebClientChonJob:
  We retrieved the following resource using Client Credentials Grant Type: This is the resource!

Мы также заметим, что при втором запуске задачи приложение запрашивает ресурс, не запрашивая сначала токен, поскольку срок действия последнего еще не истек.

5. Поддержка Spring Security 5 – Реализация с использованием потока кода авторизации

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

5.1. Конфигурации Клиента и Поставщика

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

spring.security.oauth2.client.registration.bael.client-name=bael
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret
spring.security.oauth2.client.registration.bael
  .authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.bael
  .redirect-uri=http://localhost:8080/login/oauth2/code/bael

spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token
spring.security.oauth2.client.provider.bael
  .authorization-uri=http://localhost:8085/oauth/authorize
spring.security.oauth2.client.provider.bael.user-info-uri=http://localhost:8084/user
spring.security.oauth2.client.provider.bael.user-name-attribute=name

Помимо свойств, которые мы использовали в предыдущем разделе, на этот раз нам также необходимо включить:

  • Конечная точка для проверки подлинности на сервере проверки подлинности
  • URL-адрес конечной точки, содержащий информацию о пользователе
  • URL-адрес конечной точки в нашем приложении, на которую агент пользователя будет перенаправлен после аутентификации

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

Конечная точка перенаправления создается автоматически Spring Security.

По умолчанию для него настроен URL-адрес /[action]/oauth2/code/[идентификатор регистрации], с разрешенными только authorize и login действиями (чтобы избежать бесконечного цикла).

Эта конечная точка отвечает за:

  • получение кода аутентификации в качестве параметра запроса
  • использование его для получения маркера доступа
  • создание авторизованного экземпляра клиента
  • перенаправление агента пользователя обратно в исходную конечную точку

5.2. Конфигурации безопасности HTTP

Далее нам нужно будет настроить цепочку веб-фильтров Security.

Наиболее распространенным сценарием является использование возможностей входа в систему OAuth2 Spring Security для аутентификации пользователей и предоставления им доступа к нашим конечным точкам и ресурсам.

Если это ваш случай, то достаточно просто включить директиву oauth2 Login в определение Server Http Security , чтобы наше приложение также работало в качестве клиента OAuth2:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange()
      .anyExchange()
      .authenticated()
      .and()
      .oauth2Login();
    return http.build();
}

5.3. Настройка Веб – клиента

Теперь пришло время создать наш экземпляр WebClient :

@Bean
WebClient webClient(
  ReactiveClientRegistrationRepository clientRegistrations,
  ServerOAuth2AuthorizedClientRepository authorizedClients) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        authorizedClients);
    oauth.setDefaultOAuth2AuthorizedClient(true);
    return WebClient.builder()
      .filter(oauth)
      .build();
}

На этот раз мы вводим как репозиторий регистрации клиентов, так и авторизованный репозиторий клиентов из контекста.

Мы также включаем опцию set Default OAuth2 Authorized Client . С его помощью фреймворк попытается получить информацию о клиенте из текущего объекта Authentication , управляемого в Spring Security.

Мы должны учитывать, что с его помощью все HTTP-запросы будут включать маркер доступа, что может быть нежелательным поведением.

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

5.4. Использование веб-клиента

Для выполнения процедуры Код авторизации требует агента пользователя, который может выполнять перенаправления (например, браузер).

Поэтому мы используем этот тип гранта, когда пользователь взаимодействует с нашим приложением, обычно вызывая конечную точку HTTP:

@RestController
public class ClientRestController {

    @Autowired
    WebClient webClient;

    @GetMapping("/auth-code")
    Mono useOauthWithAuthCode() {
        Mono retrievedResource = webClient.get()
          .uri("http://localhost:8084/retrieve-resource")
          .retrieve()
          .bodyToMono(String.class);
        return retrievedResource.map(string ->
          "We retrieved the following resource using Oauth: " + string);
    }
}

5.5. Тестирование

Наконец, мы вызовем конечную точку и проанализируем, что происходит, проверив записи в журнале.

После вызова конечной точки приложение проверяет, что мы еще не прошли проверку подлинности в приложении:

o.s.w.s.adapter.HttpWebHandlerAdapter: HTTP GET "/auth-code"
...
HTTP/1.1 302 Found
Location: /oauth2/authorization/bael

Приложение перенаправляется на конечную точку Службы авторизации для аутентификации с использованием учетных данных, существующих в реестрах Поставщика (в нашем случае мы будем использовать bael-user/bael-password ):

HTTP/1.1 302 Found
Location: http://localhost:8085/oauth/authorize
  ?response_type=code
  &client_id=bael-client-id
  &state=...
  &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Foauth2%2Fcode%2Fbael

После аутентификации агент пользователя отправляется обратно в URI перенаправления вместе с кодом в качестве параметра запроса и значением состояния, которое было отправлено первым (чтобы избежать CSRF-атак ):

o.s.w.s.adapter.HttpWebHandlerAdapter:HTTP GET "/login/oauth2/code/bael?code=...&state=...

Затем приложение использует код для получения маркера доступа:

o.s.w.r.f.client.ExchangeFunctions:HTTP POST http://localhost:8085/oauth/token

Он получает информацию о пользователях:

o.s.w.r.f.client.ExchangeFunctions:HTTP GET http://localhost:8084/user

И он перенаправляет агента пользователя на исходную конечную точку:

HTTP/1.1 302 Found
Location: /auth-code

Наконец, наш Веб-клиент экземпляр может успешно запросить защищенный ресурс:

o.s.w.r.f.client.ExchangeFunctions:HTTP GET http://localhost:8084/retrieve-resource
o.s.w.r.f.client.ExchangeFunctions:Response 200 OK
o.s.core.codec.StringDecoder :Decoded "This is the resource!"

6. Альтернатива – Регистрация Клиента в Звонке

Ранее мы видели, что использование установленного по умолчанию авторизованного клиента OAuth2 подразумевает, что приложение будет включать маркер доступа в любой вызов, который мы реализуем с клиентом.

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

Одним из способов, конечно, является использование clientRegistrationId , как мы делали раньше при работе с потоком учетных данных клиента.

Поскольку мы связали Главный с авторизованными клиентами мы можем получить Авторизованный клиент OAuth2 экземпляр, использующий @RegisteredOAuth2AuthorizedClient аннотация:

@GetMapping("/auth-code-annotated")
Mono useOauthWithAuthCodeAndAnnotation(
  @RegisteredOAuth2AuthorizedClient("bael") OAuth2AuthorizedClient authorizedClient) {
    Mono retrievedResource = webClient.get()
      .uri("http://localhost:8084/retrieve-resource")
      .attributes(
        ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient))
      .retrieve()
      .bodyToMono(String.class);
    return retrievedResource.map(string -> 
      "Resource: " + string 
        + " - Principal associated: " + authorizedClient.getPrincipalName() 
        + " - Token will expire at: " + authorizedClient.getAccessToken()
          .getExpiresAt());
}

7. Избегайте функций входа в систему OAuth2

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

Но что, если мы хотим избежать этого, но все же сможем получить доступ к защищенным ресурсам с помощью протокола OAuth2? Затем нам нужно будет внести некоторые изменения в нашу конфигурацию.

Для начала, и просто для ясности по всем направлениям, мы можем использовать действие authorize вместо действия login при определении свойства URI перенаправления:

spring.security.oauth2.client.registration.bael
  .redirect-uri=http://localhost:8080/login/oauth2/code/bael

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

Теперь мы настроим Цепочка Веб-фильтров Безопасности без учета вход в систему oauth2 команда, и вместо этого мы включим oauth2Client один.

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

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange()
      .anyExchange()
      .authenticated()
      .and()
      .oauth2Client()
      .and()
      .formLogin();
    return http.build();
}

Теперь давайте запустим приложение и проверим, что происходит, когда мы используем конечную точку /auth-code-annotated|/.

Сначала нам нужно будет войти в наше приложение, используя форму входа в систему.

После этого приложение перенаправит нас на вход в Службу авторизации, чтобы предоставить доступ к нашим ресурсам.

Примечание: после этого мы должны быть перенаправлены обратно к исходной конечной точке, которую мы вызвали. Тем не менее, Spring Security, похоже, перенаправляет обратно на корневой путь”/”, что, похоже, является ошибкой. Следующие запросы после запуска танца OAuth2 будут успешно выполнены.

В ответе конечной точки мы видим, что авторизованный клиент на этот раз связан с участником с именем bael-client-id вместо bael-user, названного в честь пользователя, настроенного в Службе аутентификации.

8. Поддержка Spring Framework – Ручной Подход

Из коробки Spring 5 предоставляет только один метод обслуживания, связанный с OAuth2, чтобы легко добавить заголовок токена на предъявителя в запрос. Это метод Http Headers#set Bearer Auth .

Теперь мы рассмотрим пример, чтобы понять, что потребуется для получения нашего защищенного ресурса, выполнив танец OAuth2 вручную.

Проще говоря, нам нужно будет связать два HTTP-запроса: один для получения токена аутентификации с Сервера авторизации, а другой для получения ресурса с помощью этого токена:

@Autowired
WebClient client;

public Mono obtainSecuredResource() {
    String encodedClientData = 
      Base64Utils.encodeToString("bael-client-id:bael-secret".getBytes());
    Mono resource = client.post()
      .uri("localhost:8085/oauth/token")
      .header("Authorization", "Basic " + encodedClientData)
      .body(BodyInserters.fromFormData("grant_type", "client_credentials"))
      .retrieve()
      .bodyToMono(JsonNode.class)
      .flatMap(tokenResponse -> {
          String accessTokenValue = tokenResponse.get("access_token")
            .textValue();
          return client.get()
            .uri("localhost:8084/retrieve-resource")
            .headers(h -> h.setBearerAuth(accessTokenValue))
            .retrieve()
            .bodyToMono(String.class);
        });
    return resource.map(res ->
      "Retrieved the resource using a manual approach: " + res);
}

Этот пример в основном предназначен для того, чтобы понять, насколько громоздким может быть использование запроса в соответствии со спецификацией OAuth2, и посмотреть, как используется метод setBearerAuth .

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

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

В этом руководстве мы рассмотрели, как мы можем настроить наше приложение в качестве клиента OAuth2, и, в частности, как мы можем настроить и использовать WebClient для извлечения защищенного ресурса в полнореактивном стеке.

И последнее, но не менее важное: мы проанализировали, как механизмы Spring Security 5 OAuth2 работают под капотом, чтобы соответствовать спецификации OAuth2.

Как всегда, полный пример доступен на Github .