Команда Spring АйО перевела статью, в которой Steve Reisenberg рассказал о множестве полезных улучшений для работы с OAuth2 в Security 6.4.
Прочитав статью вы узнаете, как отправлять запросы к защищенным ресурсам без дополнительных зависимостей, а также переопределять параметры в запросах токенов доступа для реализации сложных сценариев.


В версиях Spring Security 6.2 и 6.3 была упрощена конфигурация для приложений, использующих OAuth2 Client. Теперь стандартные сценарии использования стали проще благодаря возможности объявления бинов, которые автоматически включаются в общую конфигурацию OAuth2 Client при запуске приложения. Среди недавних улучшений:

  • Для включения extension grant types достаточно объявить бин типа OAuth2AuthorizedClientProvider (или ReactiveOAuth2AuthorizedClientProvider).

  • Запросы на получение токенов доступа OAuth 2.0 можно расширить кастомными параметрами, объявив один или несколько бинов типа OAuth2AccessTokenResponseClient (или ReactiveOAuth2AccessTokenResponseClient).

  • Spring Security автоматически объявляет бин типа OAuth2AuthorizedClientManager (или ReactiveOAuth2AuthorizedClientManager), если он еще не определен, благодаря чему сокращается объем шаблонного кода для получения токенов доступа.

В Spring Security 6.4 продолжается улучшение работы с RestClient — новым HTTP-клиентом, представленным в Spring Framework 6.1. RestClient предлагает удобный, похожий на WebClient, синхронный API, который не требует реактивных библиотек. Это значительно упрощает настройку запросов к защищенным ресурсам с использованием OAuth2 Client, исключая необходимость в дополнительных зависимостях.

Кроме того, были внесены изменения, чтобы обеспечить согласованность конфигураций для servlet-приложений с RestClient и реактивных приложений с WebClient. Это шаг к унификации обеих моделей конфигурации.

Давайте подробнее рассмотрим новые возможности RestClient и другие улучшения для OAuth2 Client в Spring Security 6.4.

Краткое введение в OAuth2

Для начала давайте коротко разберем основные понятия OAuth2, которые нам понадобятся.

В терминах OAuth2 запрос к защищенным ресурсам означает включение токена доступа в заголовок Authorization исходящего запроса к серверу ресурсов. Приложение, которое отправляет этот запрос, называется клиентом, потому что оно инициирует эти запросы. Приложение, к которому направляется запрос, называется сервером ресурсов, потому что оно предоставляет API для доступа к ресурсам (например, данным), принадлежащим владельцу ресурса (например, пользователю) и защищенным сервером авторизации. Сервер авторизации — это система, которая отвечает за создание и управление токенами доступа, представляющими разрешение на доступ. Сервер выдает эти токены в ответ на запросы клиента (называемые запросами токена доступа OAuth 2.0), отправленные от имени владельца ресурса.

Комментарий от редакции Spring АйО:

Authorization Flow (или Authorization Grant Type) — это процесс, позволяющий получить доступ к данным через набор шагов специфичных для конкретных флоу. Всего существует пять основных вариантов:

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

Используем RestClient для запросов к защищенным ресурсам

После краткого введения давайте посмотрим, как настроить приложение для отправки запросов к защищенным ресурсам с использованием RestClient в Spring Security 6.4. Для начала создайте новое приложение на сайте Spring Initializr. Если вы обновляете существующее приложение с Spring Boot, вам нужно будет добавить следующую зависимость:

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

Для работы приложения требуется как минимум одна настройка ClientRegistration, которая конфигурируется через бин ClientRegistrationRepository. Класс ClientRegistration является моделью в Spring Security и содержит данные для конкретного клиента OAuth2. Каждый клиент должен быть заранее зарегистрирован на сервере авторизации, а этот класс хранит данные, такие как clientId и clientSecret. Также он включает тип авторизации (authorizationGrantType), например authorization_code или client_credentials, и несколько дополнительных параметров, которые можно настроить при необходимости.

Пример ниже показывает, как настроить бин InMemoryClientRegistrationRepository с помощью ClientRegistration, используя свойства конфигурации Spring Boot:

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client:
            provider: spring
            client-id: client1
            client-secret: my-secret
            authorization-grant-type: authorization_code
            scope: message.read,message.write
        provider:
          spring:
            issuer-uri: http://localhost:9000

Указанная выше конфигурация позволяет Spring Security получать токены доступа с помощью authorization-grant-type со значением authorization_code через локальный сервер авторизации.

Spring Security предоставляет реализации OAuth2AuthorizedClientManager. Этот компонент используется для получения токенов доступа (например, JWT). Spring Security автоматически создает экземпляр этого компонента в виде бина. Нам остается только внедрить его в нашу конфигурацию, чтобы настроить RestClient для выполнения запросов к защищенным ресурсам в приложении. Пример ниже показывает минимальную настройку RestClient и его объявление в виде бина:

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(RestClient.Builder builder, 
                                 OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
			new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		return builder.requestInterceptor(requestInterceptor).build();
	}

}

Теперь мы можем отправлять запросы к защищенным ресурсам прямо из нашего приложения. Пример ниже показывает, как реализовать это в контроллере Spring MVC:

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@RestController
public class MessagesController {

	private final RestClient restClient;

	public MessagesController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/messages")
	public ResponseEntity<List<Message>> messages() {
		Message[] messages = this.restClient.get()
			.uri("http://localhost:8090/messages")
			.attributes(clientRegistrationId("messaging-client"))
			.retrieve()
			.body(Message[].class);
		return ResponseEntity.ok(Arrays.asList(messages));
	}

	public record Message(String message) {}

}

В приведённом примере используется статический метод для передачи registrationId клиента с идентификатором messaging-client через атрибуты в интерцептор. Значение registrationId совпадает с тем, что указано в YAML-конфигурации, что позволяет Spring Security определить, какой клиентский ID, секрет, authorization grant type, области доступа и другую информацию использовать для получения токена доступа.

Этот подход — всего лишь пример. Вы не ограничены вызовом запросов к защищенным ресурсам только из контроллера. Вы можете реализовать их в любой части приложения, например, в классе с аннотацией @Service или @Component, который отвечает за взаимодействие с защищенными ресурсами и возвращает результаты обратно в приложение.

Настройка запросов токена доступа OAuth 2.0 с использованием RestClient

До версии Spring Security 6.4, для servlet’ов по умолчанию использовался HTTP-клиент RestTemplate. Настройка запросов на получение OAuth 2.0 токенов доступа в servlet-приложениях с помощью RestTemplate значительно отличается от настройки в реактивных приложениях, где используется WebClient. Это связано с различиями в API между RestTemplate и WebClient.

С появлением RestClient в Spring Framework 6.1 появилась возможность унифицировать конфигурацию для двух стеков. RestClient используется для стека servlet’ов, а WebClient — для реактивного, что делает их настройку схожей. Если нужно, RestClient можно создать на основе RestTemplate с помощью метода RestClient.create(RestTemplate). Это упрощает переход к единой модели конфигурации, что является одной из целей для Spring Security 7.

В Spring Security 6.4 появились новые реализации OAuth2AccessTokenResponseClient для работы с RestClient. Вы можете настроить использование RestClient как HTTP-клиента для всех функций OAuth2 в servlet-приложении. Вот пример минимальной конфигурации, чтобы включить эту поддержку и использовать кастомный экземпляр RestClient:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	private final RestClient restClient;

	@PostConstruct
	void initialize() {
		this.restClient = RestClient.builder()
			.messageConverters((messageConverters) -> {
				messageConverters.clear();
				messageConverters.add(new FormHttpMessageConverter());
				messageConverters.add(new OAuth2AccessTokenResponseHttpMessageConverter());
			})
			.defaultStatusHandler(new OAuth2ErrorResponseErrorHandler())
			// TODO: Customize the instance of RestClient as needed...
			.build();
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		RestClientAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new RestClientAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);
		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
		RestClientRefreshTokenTokenResponseClient accessTokenResponseClient =
			new RestClientRefreshTokenTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);
		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		RestClientClientCredentialsTokenResponseClient accessTokenResponseClient =
			new RestClientClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);
		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
		return (grantRequest) -> {
			throw new UnsupportedOperationException("The `password` grant type is not supported.");
		};
	}

	@Bean
	public OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
		RestClientJwtBearerTokenResponseClient accessTokenResponseClient =
			new RestClientJwtBearerTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);
		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
		RestClientTokenExchangeTokenResponseClient accessTokenResponseClient =
			new RestClientTokenExchangeTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);
		return accessTokenResponseClient;
	}

}
Примечание:

Реализация authorization grant type password отсутствует в новой поддержке, так как существующая реализация этого authorization grant type устарела и будет удалена в Spring Security 7.

Переопределение или исключение стандартных параметров

Spring Security поддерживает несколько authorization grant types через реализации интерфейса OAuth2AccessTokenResponseClient (или ReactiveOAuth2AccessTokenResponseClient). Часто требуется изменить параметры запроса токена доступа OAuth 2.0, особенно если сервер авторизации предъявляет особые требования или предоставляет функции, которые не учтены в поддерживаемых спецификациях.

В Spring Security версии 6.3 и более ранних, для реактивных приложений было невозможно переопределить или исключить параметры, которые устанавливаются Spring Security по умолчанию. Из-за чего приходилось прибегать к использованию обходных решений для настройки приложения под такие сценарии. Теперь в версиях Spring Security, начиная с 6.4, можно переопределять параметры как для реактивных приложений (используя WebClient), так и для servlet-приложений (используя RestClient) с помощью setParametersConverter(). При этом сначала устанавливаются все параметры, специфичные для authorization grant type, а также параметры по умолчанию. Любые параметры, предоставленные вашим parametersConverter, заменят существующие.

Помимо переопределения, теперь можно исключать параметры, которые могут быть отклонены сервером авторизации. Например, если метод аутентификации клиента (ClientRegistration#clientAuthenticationMethod) установлен как private_key_jwt, можно выполнить аутентификацию клиента, используя assertion с сгенерированным JWT. Некоторые серверы авторизации могут отклонять запросы, содержащие одновременно параметры client_id и client_assertion. В данном случае, поскольку client_id является стандартным параметром, добавляемым Spring Security, необходимо исключить этот параметр, зная, что аутентификация клиента будет выполнена через assertion.

В Spring Security версии 6.4 появилась возможность исключать параметры запроса токена доступа OAuth 2.0 с помощью настройки setParametersCustomizer(). В следующем примере показано, как исключить параметр client_id, если используется клиентское утверждение для аутентификации клиента с authorization grant type client_credentials:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
		accessTokenResponseClient.addParametersConverter(
			new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver()));
		accessTokenResponseClient.setParametersCustomizer((parameters) -> {
			if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
				parameters.remove(OAuth2ParameterNames.CLIENT_ID);
			}
		});
		return accessTokenResponseClient;
	}

	private Function<ClientRegistration, JWK> jwkResolver() {
		// ...
	}

}
Примечание:

Для servlet-приложений вы можете настроить аналогичную конфигурацию, используя RestClientClientCredentialsTokenResponseClient (или другие реализации для разных authorization grant types).

Заключение

Spring Security 6.4 – это захватывающий релиз с множеством улучшений для приложений, защищенных с помощью OAuth2, а также с другими интересными нововведениями. В этой статье мы рассмотрели три новые функции из предстоящего релиза. Во-первых, мы обсудили, как отправлять запросы к защищенным ресурсам с помощью RestClient в нереактивных приложениях без необходимости добавлять дополнительные зависимости. Затем мы изучили возможность использовать RestClient повсеместно, что упрощает настройку и делает ее более согласованной с реактивным подходом. Наконец, мы узнали, как переопределять или исключать стандартные параметры в запросах токенов доступа OAuth 2.0, что позволяет реализовывать сложные сценарии, которые ранее были трудно достижимыми.

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

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь!

Комментарии (0)