Настройка авторизации и запросов токенов с помощью Spring Security 5.1 Client

Настройка авторизации и запросов токенов с помощью клиента Spring Security 5.1

1. обзор

Иногда API-интерфейсы OAuth2 могут немного отличаться от стандартных, и в этом случае нам необходимо выполнить некоторые настройки для стандартных запросов OAuth2.

Spring Security 5.1 обеспечивает поддержку настройки авторизации OAuth2 и запросов токенов.

В этом руководстве мы увидим, как настроить параметры запроса и обработку ответов.

2. Пользовательский запрос авторизации

Сначала мы настроим запрос авторизации OAuth2. Мы можем изменять стандартные параметры и добавлять дополнительные параметры в запрос на авторизацию по мере необходимости.

Для этогоwe need to implement our own OAuth2AuthorizationRequestResolver:

public class CustomAuthorizationRequestResolver
  implements OAuth2AuthorizationRequestResolver {

    private OAuth2AuthorizationRequestResolver defaultResolver;

    public CustomAuthorizationRequestResolver(
      ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
        defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
    }

    // ...
}

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

Мы также переопределим методыresolve(), чтобы добавить нашу логику настройки:

public class CustomAuthorizationRequestResolver
  implements OAuth2AuthorizationRequestResolver {

    //...

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
        if(req != null) {
            req = customizeAuthorizationRequest(req);
        }
        return req;
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
        if(req != null) {
            req = customizeAuthorizationRequest(req);
        }
        return req;
    }

    private OAuth2AuthorizationRequest customizeAuthorizationRequest(
      OAuth2AuthorizationRequest req) {
        // ...
    }

}

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

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

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.oauth2Login()
          .authorizationEndpoint()
          .authorizationRequestResolver(
            new CustomAuthorizationRequestResolver(
              clientRegistrationRepository(), "/oauth2/authorize-client"))
        //...
    }
}

Здесь мы использовалиoauth2Login().authorizationEndpoint().authorizationRequestResolver() для внедрения нашего пользовательскогоOAuth2AuthorizationRequestResolver.

3. Настройка стандартных параметров запроса на авторизацию

Теперь давайте обсудим собственно настройку. Мы можем изменятьOAuth2AuthorizationRequest сколько угодно.

Для началаwe can modify a standard parameter for each authorization request.

Мы можем, например, сгенерировать собственный параметр“state”:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    return OAuth2AuthorizationRequest
      .from(req).state("xyz").build();
}

4. Дополнительные параметры запроса авторизации

We can also add extra parameters to our OAuth2AuthorizationRequest, используя методadditionalParameters() изOAuth2AuthorizationRequest и передаваяMap:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    Map extraParams = new HashMap();
    extraParams.putAll(req.getAdditionalParameters());
    extraParams.put("test", "extra");

    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

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

Давайте посмотрим на более практичный пример, настроив запрос авторизации, используемый с сервером авторизации Okta.

4.1. Пользовательский запрос на авторизацию Okta

Okta имеет дополнительные необязательные параметры для запроса авторизации, чтобы предоставить пользователю больше функций. Например,idp, который указывает поставщика удостоверений.

По умолчанию поставщиком удостоверений является Okta, но мы можем настроить его с помощью параметраidp:

private OAuth2AuthorizationRequest customizeOktaReq(OAuth2AuthorizationRequest req) {
    Map extraParams = new HashMap();
    extraParams.putAll(req.getAdditionalParameters());
    extraParams.put("idp", "https://idprovider.com");
    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

5. Пользовательский запрос токена

Теперь посмотрим, как настроить запрос токена OAuth2.

Мы можем настроить запрос токена, настроивOAuth2AccessTokenResponseClient.

Реализация по умолчанию дляOAuth2AccessTokenResponseClient -DefaultAuthorizationCodeTokenResponseClient.

Мы можем настроить сам запрос токена, предоставив собственныйRequestEntityConverter, и мы даже можем настроить обработку ответа токена, настроивDefaultAuthorizationCodeTokenResponseClientRestOperations:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.tokenEndpoint()
          .accessTokenResponseClient(accessTokenResponseClient())
            //...
    }

    @Bean
    public OAuth2AccessTokenResponseClient accessTokenResponseClient(){
        DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
          new DefaultAuthorizationCodeTokenResponseClient();
        accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter());

        OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
          new OAuth2AccessTokenResponseHttpMessageConverter();
        tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
          new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

        accessTokenResponseClient.setRestOperations(restTemplate);
        return accessTokenResponseClient;
    }
}

Мы можем ввести собственныйOAuth2AccessTokenResponseClient usingtokenEndpoint().accessTokenResponseClient().

Чтобы настроить параметры запроса токена, мы реализуемCustomRequestEntityConverter.. Точно так же, чтобы настроить обработку ответа токена, мы реализуемCustomTokenResponseConverter.

Мы обсудим какCustomRequestEntityConverter, так иCustomTokenResponseConverter в следующих разделах.

6. Дополнительные параметры запроса токена

Теперь посмотрим, как добавить дополнительные параметры к нашему запросу токена, создав пользовательскийConverter:

public class CustomRequestEntityConverter implements
  Converter> {

    private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;

    public CustomRequestEntityConverter() {
        defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
    }

    @Override
    public RequestEntity convert(OAuth2AuthorizationCodeGrantRequest req) {
        RequestEntity entity = defaultConverter.convert(req);
        MultiValueMap params = (MultiValueMap) entity.getBody();
        params.add("test2", "extra2");
        return new RequestEntity<>(params, entity.getHeaders(),
          entity.getMethod(), entity.getUrl());
    }

}

НашConverter преобразуетOAuth2AuthorizationCodeGrantRequest вRequestEntity. 

Мы использовали преобразователь по умолчаниюOAuth2AuthorizationCodeGrantRequestEntityConverter для обеспечения базовой функциональности и добавили дополнительные параметры в телоRequestEntity.

7. Обработка пользовательских ответов на токены

Теперь мы настроим обработку ответа токена.

Мы можем использовать конвертер ответов токена по умолчаниюOAuth2AccessTokenResponseHttpMessageConverter в качестве отправной точки.

Мы реализуемCustomTokenResponseConverter для другой обработки параметра“scope”:

public class CustomTokenResponseConverter implements
  Converter, OAuth2AccessTokenResponse> {
    private static final Set TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of(
        OAuth2ParameterNames.ACCESS_TOKEN,
        OAuth2ParameterNames.TOKEN_TYPE,
        OAuth2ParameterNames.EXPIRES_IN,
        OAuth2ParameterNames.REFRESH_TOKEN,
        OAuth2ParameterNames.SCOPE).collect(Collectors.toSet());

    @Override
    public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) {
        String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

        Set scopes = Collections.emptySet();
        if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
            String scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
            scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, ","))
                .collect(Collectors.toSet());
        }

        //...
        return OAuth2AccessTokenResponse.withToken(accessToken)
          .tokenType(accessTokenType)
          .expiresIn(expiresIn)
          .scopes(scopes)
          .refreshToken(refreshToken)
          .additionalParameters(additionalParameters)
          .build();
    }

}

Конвертер ответа токена преобразуетMap вOAuth2AccessTokenResponse.

В этом примере мы проанализировали параметр“scope” как разделенный запятыми вместо разделенного пробеламиString.

Давайте рассмотрим еще один практический пример, настроив ответ токена с использованием LinkedIn в качестве сервера авторизации.

7.1. Обработка ответов на токены LinkedIn

Наконец, давайте посмотрим, как обрабатывать ответ токенаLinkedIn. Он содержит толькоaccess_token иexpires_in,, но нам также понадобитсяtoken_type.

Мы можем просто реализовать наш собственный конвертер ответа токена и установитьtoken_type вручную:

public class LinkedinTokenResponseConverter
  implements Converter, OAuth2AccessTokenResponse> {

    @Override
    public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) {
        String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
        long expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));

        OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;

        return OAuth2AccessTokenResponse.withToken(accessToken)
          .tokenType(accessTokenType)
          .expiresIn(expiresIn)
          .build();
    }
}

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

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

Полный исходный код примеров доступенover on GitHub.