1. 概述
在实际开发中,不少 OAuth2 接口可能并未完全遵循标准协议,这时候我们就需要对标准的 OAuth2 请求进行一些定制化处理。
✅ Spring Security 5.1 提供了对 OAuth2 授权请求和令牌请求的自定义支持。
本文将带你了解如何自定义请求参数以及响应处理逻辑。
2. 自定义授权请求
首先我们来定制 OAuth2 的授权请求(Authorization Request),可以修改标准参数,也可以添加额外参数。
要实现这一点,我们需要实现自己的 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()
方法来添加自定义逻辑。
完成自定义后,需要把它注册到安全配置中:
@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. 添加额外的授权请求参数
✅ 除了标准参数,还可以通过 additionalParameters()
方法添加额外参数:
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
也带上,避免覆盖。
4.1. 自定义 Okta 授权请求
以 Okta 为例,它支持一些额外的可选参数,如 idp
(identity provider):
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 Request)的自定义。
✅ 我们可以通过自定义 OAuth2AccessTokenResponseClient
来实现。
默认实现是 DefaultAuthorizationCodeTokenResponseClient
。
我们可以自定义请求参数通过 RequestEntityConverter
,也可以自定义响应处理通过 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.setAccessTokenResponseConverter(new CustomTokenResponseConverter());
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
accessTokenResponseClient.setRestOperations(restTemplate);
return accessTokenResponseClient;
}
}
✅ 注入方式是:tokenEndpoint().accessTokenResponseClient()
。
6. 添加额外的令牌请求参数
通过实现 Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>
来添加额外参数:
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());
}
}
✅ 我们使用默认转换器作为基础,然后添加自定义参数。
7. 自定义令牌响应处理
接下来是自定义令牌响应的处理。
✅ 我们可以继承默认的 OAuth2AccessTokenResponseHttpMessageConverter
实现方式。
比如,自定义 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();
}
}
7.1. LinkedIn 令牌响应处理
LinkedIn 的响应中只包含 access_token
和 expires_in
,缺少 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 的授权请求和令牌请求。
完整代码可以在 GitHub 找到。