Customizing Authorization and Token Requests with Spring Security 5.1 Client – 用Spring Security 5.1客户端定制授权和令牌请求

最后修改: 2018年 12月 2日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Sometimes OAuth2 APIs can diverge a little from the standard, in which case we need to do some customizations to the standard OAuth2 requests.

有时,OAuth2 API会与标准有一些分歧,在这种情况下,我们需要对标准的OAuth2请求做一些定制。

Spring Security 5.1 provides support for customizing OAuth2 authorization and token requests.

Spring Security 5.1提供了对定制OAuth2授权和令牌请求的支持。

In this tutorial, we’ll see how to customize request parameters and response handling.

在本教程中,我们将看到如何定制请求参数和响应处理。

2. Custom Authorization Request

2.定制授权请求

First, we’ll customize the OAuth2 authorization request. We can modify standard parameters and add extra parameters to the authorization request as we need.

首先,我们要定制OAuth2的授权请求。我们可以根据需要修改标准参数并在授权请求中添加额外的参数。

To do so, we need to implement our own OAuth2AuthorizationRequestResolver:

为此,我们需要实现我们自己的OAuth2AuthorizationRequestResolver

public class CustomAuthorizationRequestResolver 
  implements OAuth2AuthorizationRequestResolver {
    
    private OAuth2AuthorizationRequestResolver defaultResolver;

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

Note that we used the DefaultOAuth2AuthorizationRequestResolver to provide base functionality.

请注意,我们使用DefaultOAuth2AuthorizationRequestResolver来提供基础功能。

We’ll also override the resolve() methods to add our customization logic:

我们还将覆盖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) {
        // ...
    }

}

We’ll add our customizations later on using our method customizeAuthorizationRequest() method as we’ll discuss in the next sections.

我们将在以后使用我们的方法customizeAuthorizationRequest()添加我们的定制,我们将在接下来的章节中讨论。

After implementing our custom OAuth2AuthorizationRequestResolver, we need to add it to our security configuration:

在实现我们的自定义OAuth2AuthorizationRequestResolver之后,我们需要将其添加到我们的安全配置中。

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2Login()
          .authorizationEndpoint()
          .authorizationRequestResolver(
            new CustomAuthorizationRequestResolver(
              clientRegistrationRepository(), "/oauth2/authorize-client"))
        //...
    }
}

Here we used oauth2Login().authorizationEndpoint().authorizationRequestResolver() to inject our custom OAuth2AuthorizationRequestResolver.

这里我们使用oauth2Login().authorizationEndpoint().authorizationRequestResolver()来注入我们自定义的OAuth2AuthorizationRequestResolver。

3. Customizing Authorization Request Standard Parameters

3.自定义授权请求标准参数

Now, let’s discuss the actual customization. We can modify OAuth2AuthorizationRequest as much as we want.

现在,让我们来讨论一下实际的定制工作。我们可以尽情地修改OAuth2AuthorizationRequest

For starters, we can modify a standard parameter for each authorization request.

对于初学者来说,我们可以为每个授权请求修改一个标准参数。

We can, for example, generate our own “state” parameter:

例如,我们可以生成我们自己的“state”参数。

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

4. Authorization Request Extra Parameters

4.授权请求额外参数

We can also add extra parameters to our OAuth2AuthorizationRequest using the additionalParameters() method of the OAuth2AuthorizationRequest and passing in a Map:

我们还可以使用OAuth2AuthorizationRequestadditionalParameters()方法向我们的OAuth2AuthorizationRequest添加额外的参数,并传入Map:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    Map<String,Object> extraParams = new HashMap<String,Object>();
    extraParams.putAll(req.getAdditionalParameters()); 
    extraParams.put("test", "extra");
    
    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

We also have to make sure that we include the old additionalParameters before we add our new ones.

我们还必须确保在添加新的参数之前,我们包括旧的additionalParameters

Let’s see a more practical example by customizing the authorization request used with the Okta Authorization Server.

让我们通过定制Okta授权服务器使用的授权请求来看一个更实际的例子。

4.1. Custom Okta Authorize Request

4.1.自定义Okta授权请求

Okta has extra optional parameters for authorization request to provide the user with more functionality. For example, idp which indicates the identity provider.

Okta为授权请求提供了额外的可选参数,以便为用户提供更多功能。例如,idp表示身份提供者。

The identity provider is Okta by default, but we can customize it using idp parameter:

身份提供者默认为Okta,但我们可以使用idp参数进行自定义。

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

5. Custom Token Request

5.自定义令牌请求

Now, we’ll see how to customize the OAuth2 token request.

现在,我们将看到如何定制OAuth2令牌请求。

We can customize the token request by customizing OAuth2AccessTokenResponseClient.

我们可以通过定制OAuth2AccessTokenResponseClient来定制令牌请求。

The default implementation for OAuth2AccessTokenResponseClient is DefaultAuthorizationCodeTokenResponseClient.

OAuth2AccessTokenResponseClient的默认实现是DefaultAuthorizationCodeTokenResponseClient

We can customize the token request itself by providing a custom RequestEntityConverter and we can even customize the token response handling by customizing DefaultAuthorizationCodeTokenResponseClient RestOperations:

我们可以通过提供一个自定义的RequestEntityConverter来定制令牌请求本身,我们甚至可以通过自定义DefaultAuthorizationCodeTokenResponseClient RestOperations来定制令牌响应处理。

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.tokenEndpoint()
          .accessTokenResponseClient(accessTokenResponseClient())
            //...
    }

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> 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;
    }
}

We can inject our own OAuth2AccessTokenResponseClient using tokenEndpoint().accessTokenResponseClient().

我们可以使用tokenEndpoint().accessTokenResponseClient()注入我们自己的OAuth2AccessTokenResponseClient/strong>

To customize token request parameters, we’ll implement CustomRequestEntityConverter. Similarly, to customize handling token response, we’ll implement CustomTokenResponseConverter.

为了定制令牌请求参数,我们将实现CustomRequestEntityConverter。类似地,为了定制处理令牌响应,我们将实现CustomTokenResponseConverter.

We’ll discuss both CustomRequestEntityConverter and CustomTokenResponseConverter in the following sections.

我们将在以下章节中讨论CustomRequestEntityConverterCustomTokenResponseConverter

6. Token Request Extra Parameters

6.令牌请求额外参数

Now, we’ll see how to add extra parameters to our token request by building a custom Converter:

现在,我们将看到如何通过建立一个自定义的Converter来为我们的token请求添加额外的参数。

public class CustomRequestEntityConverter implements 
  Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {

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

}

Our Converter transforms OAuth2AuthorizationCodeGrantRequest to a RequestEntity. 

我们的转换器OAuth2AuthorizationCodeGrantRequest转换为一个RequestEntity。

We used default converter OAuth2AuthorizationCodeGrantRequestEntityConverter to provide base functionality, and added extra parameters to the RequestEntity body.

我们使用默认的转换器OAuth2AuthorizationCodeGrantRequestEntityConverter来提供基本功能,并在RequestEntity体中添加额外的参数。

7. Custom Token Response Handling

7.自定义令牌响应处理

Now, we’ll customize handling the token response.

现在,我们将自定义处理令牌响应。

We can use the default token response converter OAuth2AccessTokenResponseHttpMessageConverter as a starting point.

我们可以使用默认的令牌响应转换器OAuth2AccessTokenResponseHttpMessageConverter作为一个起点。

We’ll implement CustomTokenResponseConverter to handle the “scope” parameter differently:

我们将实现CustomTokenResponseConverter,以不同方式处理“范围”参数:

public class CustomTokenResponseConverter implements 
  Converter<Map<String, String>, OAuth2AccessTokenResponse> {
    private static final Set<String> 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<String, String> tokenResponseParameters) {
        String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

        Set<String> 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();
    }

}

The token response converter transforms Map to OAuth2AccessTokenResponse.

令牌响应转换器将Map转换为OAuth2AccessTokenResponse.

In this example, we parsed the “scope” parameter as a comma-delimited instead of space-delimited String.

在这个例子中,我们将“范围”参数解析为以逗号分隔而不是以空格分隔的字符串。

Let’s go through another practical example by customizing the token response using LinkedIn as an authorization server.

让我们通过使用LinkedIn作为授权服务器来定制令牌响应的另一个实际例子。

7.1. LinkedIn Token Response Handling

7.1 LinkedIn令牌响应处理

Finally, let’s see how to handle the LinkedIn token response. This contains only access_token and expires_in, but we also need token_type.

最后,让我们看看如何处理LinkedIn令牌响应。这只包含access_tokenexpires_in,但我们还需要token_type.

We can simply implement our own token response converter and set token_type manually:

我们可以简单地实现我们自己的令牌响应转换器并手动设置token_type

public class LinkedinTokenResponseConverter 
  implements Converter<Map<String, String>, OAuth2AccessTokenResponse> {

    @Override
    public OAuth2AccessTokenResponse convert(Map<String, String> 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. Conclusion

8.结论

In this article, we learned how to customize OAuth2 authorization and token requests by adding or modifying request parameters.

在这篇文章中,我们学习了如何通过添加或修改请求参数来定制OAuth2授权和令牌请求。

The full source code for the examples is available over on GitHub.

例子的完整源代码可在GitHub上获得over