知識庫 / Spring / Spring Security RSS 訂閱

使用 Spring Security Client 自定義授權和令牌請求

Spring Security
HongKong
5
01:35 PM · Dec 06 ,2025

1. 概述

有時 OAuth2 API 可能會與標準略有偏差,因此我們需要對標準 OAuth2 請求進行一些自定義。

Spring Security 5.1 提供對 OAuth2 授權和令牌請求的自定義支持。

在本教程中,我們將學習如何自定義請求參數和響應處理。

2. 自定義授權請求

首先,我們將自定義OAuth2授權請求。我們可以修改標準參數並向授權請求中添加所需的額外參數。

為此,我們需要實現我們自己的 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 {

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

我們使用了 oauth2Login().authorizationEndpoint().authorizationRequestResolver() 來注入我們自定義的 OAuth2AuthorizationRequestResolver

3. 定製化授權請求標準參數

現在,我們來討論實際的定製化。我們可以隨意修改 OAuth2AuthorizationRequest

為了開始,我們可以為每個授權請求修改標準參數。

例如,我們可以生成自定義的 “state” 參數:

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

4. 授權請求:額外參數

我們還可以使用 OAuth2AuthorizationRequestadditionalParameters() 方法,並傳入一個 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();
}

我們還需要確保在添加新的參數之前,包含舊的 additionalParameters

下面我們通過定製用於與 Okta 授權服務器一起使用的授權請求,來提供一個更實際的示例。

4.1. 自定義 Okta 授權請求

Okta 提供額外的可選參數,用於授權請求,從而為用户提供更多功能。例如,idp,它指示身份提供商。

默認情況下,身份提供商為 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. 自定義令牌請求

現在,我們將瞭解如何自定義 OAuth2 令牌請求。

我們可以通過自定義&lt;em&gt;OAuth2AccessTokenResponseClient&lt;/em&gt; 來自定義令牌請求。

對於&lt;em&gt;OAuth2AccessTokenResponseClient&lt;/em&gt; 的默認實現是&lt;em&gt;DefaultAuthorizationCodeTokenResponseClient&lt;/em&gt;。

我們可以通過提供自定義的 RequestEntityConverter 來自定義令牌請求本身,並且還可以通過自定義 DefaultAuthorizationCodeTokenResponseClientRestOperations 來自定義令牌響應處理:

@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.setAccessTokenResponseConverter(new CustomTokenResponseConverter()); 
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
          new FormHttpMessageConverter(), tokenResponseHttpMessageConverter)); 
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); 
        
        accessTokenResponseClient.setRestOperations(restTemplate); 
        return accessTokenResponseClient;
    }
}

我們可以通過OAuth2AccessTokenResponseClient來注入tokenEndpoint().accessTokenResponseClient().

為了自定義令牌請求參數,我們將實現CustomRequestEntityConverter。 同樣,為了自定義令牌響應處理,我們將實現CustomTokenResponseConverter

我們將討論CustomRequestEntityConverterCustomTokenResponseConverter這兩種轉換器,具體內容將在後續章節中進行闡述。

6. 令牌請求中的額外參數

現在,我們將探討如何通過構建自定義 Converter 來向我們的令牌請求添加額外的參數:

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

}

我們的 轉換器OAuth2授權碼請求 轉換為 請求實體

我們使用了默認的轉換器 OAuth2AuthorizationCodeGrantRequestEntityConverter 以提供基本功能,並向 請求實體 的主體添加了額外的參數。

7. 自定義令牌響應處理

現在,我們將自定義令牌響應的處理方式。

我們可以使用默認的令牌響應轉換器 《OAuth2AccessTokenResponseHttpMessageConverter》 作為起點。

我們將實現 CustomTokenResponseConverter 以處理 “scope” 參數的方式:

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

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

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

}

令牌響應轉換器將 Map 轉換為 OAuth2AccessTokenResponse

在此示例中,我們將 “scope” 參數解析為逗號分隔的值,而不是空格分隔的 String

讓我們通過使用 LinkedIn 作為授權服務器來定製令牌響應,來演示另一個實際示例。

7.1. LinkedIn 訪問令牌響應處理

最後,讓我們看看如何處理 LinkedIn 訪問令牌響應。它僅包含 access_tokenexpires_in, 但我們也需要 token_type。

我們可以簡單地實現自己的令牌響應轉換器,並手動設置 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. 結論

本文介紹瞭如何通過添加或修改請求參數來定製 OAuth2 授權和令牌請求。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.