1. 概述
有時 OAuth2 API 可能會與標準略有偏差,因此我們需要對標準 OAuth2 請求進行一些自定義。Spring Security 5.1 提供對 OAuth2 授權和令牌請求的自定義支持。
在本教程中,我們將學習如何自定義請求參數和響應處理。
2. 自定義授權請求
首先,我們將自定義 OAuth2 授權請求。我們可以修改標準參數並向授權請求添加額外的參數,以滿足我們的需求。
為此,我們需要實現我們自己的<em>OAuth2AuthorizationRequestResolver</em>:
public class CustomAuthorizationRequestResolver
implements OAuth2AuthorizationRequestResolver {
private OAuth2AuthorizationRequestResolver defaultResolver;
public CustomAuthorizationRequestResolver(
ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
}
// ...
}
請注意,我們使用了<em>DefaultOAuth2AuthorizationRequestResolver</em>來提供基本功能。
我們還將覆蓋<em>resolve()</em>方法以添加我們的自定義邏輯:
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) {
// ...
}
}
我們將使用我們的方法<em>customizeAuthorizationRequest()</em>方法在下一部分中添加我們的自定義操作。
在實現我們自定義的<em>OAuth2AuthorizationRequestResolver</em>之後,我們需要將其添加到我們的安全配置中:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(
new CustomAuthorizationRequestResolver(
clientRegistrationRepository(), "/oauth2/authorize-client"))
//...
}
}
在這裏,我們使用了<em>oauth2Login().authorizationEndpoint().authorizationRequestResolver()</em>來注入我們的自定義<em>OAuth2AuthorizationRequestResolver</em>。
3. 自定義 授權請求標準參數
現在,我們來討論實際的自定義。我們可以修改OAuth2AuthorizationRequest 的內容。
首先,我們可以修改每個授權請求的標準參數。
例如,我們可以生成自己的“state” 參數:
private OAuth2AuthorizationRequest customizeAuthorizationRequest(
OAuth2AuthorizationRequest req) {
return OAuth2AuthorizationRequest
.from(req).state("xyz").build();
}
4. 授權請求 額外參數
我們還可以使用 OAuth2AuthorizationRequest 的 additionalParameters() 方法,通過傳遞一個 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. 自定義 Token 請求
現在,我們將瞭解如何自定義 OAuth2 token 請求。
我們可以通過自定義 OAuth2AccessTokenResponseClient 來自定義 token 請求。
OAuth2AccessTokenResponseClient 的默認實現是 DefaultAuthorizationCodeTokenResponseClient。
我們可以通過提供自定義 RequestEntityConverter 來自定義 token 請求本身, 並且還可以通過自定義 DefaultAuthorizationCodeTokenResponseClient RestOperations 來自定義 token 響應處理:
@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().
為了自定義 token 請求參數,我們將實現 CustomRequestEntityConverter。 同樣,為了自定義 token 響應處理,我們將實現 CustomTokenResponseConverter。
我們將討論 CustomRequestEntityConverter 和 CustomTokenResponseConverter 在後面的章節。
6. 令牌請求額外參數
現在,我們將學習如何向我們的令牌請求添加額外的參數,通過構建自定義 轉換器: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());
}
}
我們的 轉換器OAuth2AuthorizationCodeGrantRequestRequestEntity.
我們使用了默認轉換器 OAuth2AuthorizationCodeGrantRequestEntityConverterRequestEntity
7. 定製 Token 響應處理
現在,我們將自定義處理 Token 響應。
我們可以使用默認的 Token 響應轉換器 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();
}
}
Token 響應轉換器將 Map 轉換為 OAuth2AccessTokenResponse。
在這個例子中,我們解析了 “scope” 參數為逗號分隔而不是空格分隔的 String。
讓我們通過使用 LinkedIn 作為授權服務器來定製 Token 響應來查看另一個實際例子。
7.1. LinkedIn Token Response Handling
最後,讓我們看看如何處理 LinkedIn Token 響應。它只包含 access_token 和 expires_in,但我們也需要 token_type。
我們可以簡單地實現自己的 Token 響應轉換器並手動設置 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 授權和令牌請求。