«1. Обзор

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

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

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

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

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

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

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

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

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

3. Spring Security 5 Под капотом

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

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

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

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

3.1. Поставщики

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

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

3.2. Client Registrations

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

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

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

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

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

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

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

В частности, классы ReactiveClientRegistrationRepository и ServerOAuth2AuthorizedClientRepository используются в реактивных стеках и по умолчанию используют хранилище в памяти.

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

3.5. Security Web Filter Chain

Одной из ключевых концепций Spring Security 5 является реактивная сущность SecurityWebFilterChain.

Как видно из названия, он представляет собой связанную коллекцию объектов WebFilter.

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

  1. One filter responds to authorization requests (the /oauth2/authorization/{registrationId} URI) or throws a ClientAuthorizationRequiredException. It contains a reference to the ReactiveClientRegistrationRepository, and it’s in charge of creating the authorization request to redirect the user-agent.
  2. The second filter differs depending on which feature we’re adding (OAuth2 Client capabilities or the OAuth2 Login functionality). In both cases, the main responsibility of this filter is to create the OAuth2AuthorizedClient instance and store it using the ServerOAuth2AuthorizedClientRepository.

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

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

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

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. Использование WebClient

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

Например, давайте представим, что у нас есть задание 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

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

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

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

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

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

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

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

Если мы запустим наше приложение с включенным уровнем ведения журнала DEBUG, мы сможем увидеть вызовы, которые 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/[registrationId], с разрешенными только действиями авторизации и входа (во избежание бесконечного цикла).

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

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

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

Далее нам нужно настроить SecurityWebFilterChain.

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

Если это наш случай, то простого включения директивы oauth2Login в определение ServerHttpSecurity будет достаточно, чтобы наше приложение также работало как клиент OAuth2:

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

5.3. Настройка WebClient

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

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

«

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

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

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

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

5.4. Использование WebClient

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

@RestController
public class ClientRestController {

    @Autowired
    WebClient webClient;

    @GetMapping("/auth-code")
    Mono<String> useOauthWithAuthCode() {
        Mono<String> 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);
    }
}

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

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

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

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

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

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

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

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

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

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!"

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

6. Альтернатива — регистрация клиента в вызове

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

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

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

@GetMapping("/auth-code-annotated")
Mono<String> useOauthWithAuthCodeAndAnnotation(
  @RegisteredOAuth2AuthorizedClient("bael") OAuth2AuthorizedClient authorizedClient) {
    Mono<String> 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());
}

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

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

Для начала и просто для ясности по всем направлениям мы можем использовать действие авторизации вместо входа в систему при определении свойства URI перенаправления:

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

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

Теперь мы настроим SecurityWebFilterChain без включения команды oauth2Login, а вместо этого включим команду oauth2Client.

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

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

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

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

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

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

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

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

По умолчанию Spring 5 предоставляет только один сервисный метод, связанный с OAuth2, для простого добавления заголовка токена Bearer в запрос. Это метод HttpHeaders#setBearerAuth.

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

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

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

@Autowired
WebClient client;

public Mono<String> obtainSecuredResource() {
    String encodedClientData = 
      Base64Utils.encodeToString("bael-client-id:bael-secret".getBytes());
    Mono<String> 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);
}

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

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

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

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

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

«