如何为RestClientAuthorizationCodeTokenResponseClient配置自定义令牌响应转换器?
问题描述
我已基于Spring Security OAuth2 Client实现了对接Datev应用的OAuth认证功能,编写了自定义令牌请求参数转换器和令牌响应转换器,其中令牌请求参数转换器已成功配置生效。但RestClientAuthorizationCodeTokenResponseClient类未提供设置令牌响应消息转换器的方法,而我的自定义响应转换器需要处理refresh_token_expires_in等额外字段,无法将其应用到令牌响应处理流程中。请问如何为RestClientAuthorizationCodeTokenResponseClient配置该自定义令牌响应转换器?或者有无更优的getTokenResponse逻辑处理方案?
相关代码
CustomAuthorizationCodeTokenResponseClient.java
import com.benehr.api.model.User; import com.benehr.api.repository.UserRepository; import com.benehr.api.service.datev.DatevTokenService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.security.oauth2.client.endpoint.*; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @RequiredArgsConstructor public class CustomAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> { private final RestClientAuthorizationCodeTokenResponseClient delegate = new RestClientAuthorizationCodeTokenResponseClient(); private final DatevTokenService datevTokenService; private final UserRepository userRepository; @Override public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpSession session = request.getSession(false); final String codeVerifier = (session != null) ? (String) session.getAttribute("code_verifier") : null; if (codeVerifier == null) { throw new IllegalStateException("code_verifier not found in session"); } CustomAccessTokenResponseConverter responseConverter = new CustomAccessTokenResponseConverter(); delegate.setParametersConverter(new CustomTokenRequestParametersConverter(codeVerifier)); OAuth2AccessTokenResponse tokenResponse = delegate.getTokenResponse(authorizationGrantRequest); return tokenResponse; } }
CustomTokenRequestParametersConverter.java
import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.client.endpoint.DefaultOAuth2TokenRequestParametersConverter; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; public class CustomTokenRequestParametersConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> { private final DefaultOAuth2TokenRequestParametersConverter<OAuth2AuthorizationCodeGrantRequest> defaultConverter = new DefaultOAuth2TokenRequestParametersConverter<>(); private final String codeVerifier; public CustomTokenRequestParametersConverter(String codeVerifier) { this.codeVerifier = codeVerifier; } @Override public MultiValueMap<String, String> convert(OAuth2AuthorizationCodeGrantRequest grantRequest) { MultiValueMap<String, String> params = defaultConverter.convert(grantRequest); if (!(params instanceof LinkedMultiValueMap)) { params = new LinkedMultiValueMap<>(params); } params.add("code_verifier", codeVerifier); return params; } }
CustomAccessTokenResponseConverter.java
import com.benehr.api.dto.response.datev.DatevTokenResponse; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; public class CustomAccessTokenResponseConverter extends OAuth2AccessTokenResponseHttpMessageConverter { @Override protected OAuth2AccessTokenResponse readInternal(Class<? extends OAuth2AccessTokenResponse> clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException { try { ObjectMapper objectMapper = new ObjectMapper(); DatevTokenResponse tokenResponse = objectMapper.readValue(inputMessage.getBody(), DatevTokenResponse.class); if (tokenResponse.access_token() == null) { throw new HttpMessageNotReadableException("Missing access_token in token response", inputMessage); } Map<String, Object> additionalParameters = new HashMap<>(); if (tokenResponse.id_token() != null) { additionalParameters.put("id_token", tokenResponse.id_token()); } if (tokenResponse.refresh_token_expires_in() != null) { additionalParameters.put("refresh_token_expires_in", tokenResponse.refresh_token_expires_in()); } if (tokenResponse.scope() != null) { additionalParameters.put("scope", tokenResponse.scope()); } Set<String> scopes = Collections.emptySet(); if (tokenResponse.scope() != null && !tokenResponse.scope().isEmpty()) { scopes = Set.of(tokenResponse.scope().split(" ")); } OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER; if (tokenResponse.token_type() != null) { String tokenTypeStr = tokenResponse.token_type().toUpperCase(); if ("BEARER".equals(tokenTypeStr)) { tokenType = OAuth2AccessToken.TokenType.BEARER; } } return OAuth2AccessTokenResponse.withToken(tokenResponse.access_token()) .tokenType(tokenType) .expiresIn(tokenResponse.expires_in() != null ? tokenResponse.expires_in() : 0) .refreshToken(tokenResponse.refresh_token()) .scopes(scopes) .additionalParameters(additionalParameters) .build(); } catch (IOException e) { throw new HttpMessageNotReadableException("Failed to read OAuth2AccessTokenResponse", e, inputMessage); } } }
解决方案
方案一:通过自定义RestClient注入响应转换器
RestClientAuthorizationCodeTokenResponseClient虽无直接设置响应转换器的方法,但可通过setRestClient()传入自定义配置的RestClient,在其中注册你的CustomAccessTokenResponseConverter。修改后的CustomAuthorizationCodeTokenResponseClient如下:
@RequiredArgsConstructor public class CustomAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> { private final RestClientAuthorizationCodeTokenResponseClient delegate = new RestClientAuthorizationCodeTokenResponseClient(); private final DatevTokenService datevTokenService; private final UserRepository userRepository; // 构造方法中初始化自定义RestClient public CustomAuthorizationCodeTokenResponseClient(DatevTokenService datevTokenService, UserRepository userRepository) { this.datevTokenService = datevTokenService; this.userRepository = userRepository; // 配置RestClient,替换默认响应转换器 RestClient restClient = RestClient.builder() .messageConverters(converters -> { // 移除默认的OAuth2AccessTokenResponseHttpMessageConverter converters.removeIf(converter -> converter instanceof OAuth2AccessTokenResponseHttpMessageConverter); // 添加自定义响应转换器 converters.add(new CustomAccessTokenResponseConverter()); }) .build(); delegate.setRestClient(restClient); } @Override public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpSession session = request.getSession(false); final String codeVerifier = (session != null) ? (String) session.getAttribute("code_verifier") : null; if (codeVerifier == null) { throw new IllegalStateException("code_verifier not found in session"); } delegate.setParametersConverter(new CustomTokenRequestParametersConverter(codeVerifier)); OAuth2AccessTokenResponse tokenResponse = delegate.getTokenResponse(authorizationGrantRequest); // 可在此处调用datevTokenService处理额外字段,比如保存refresh_token_expires_in return tokenResponse; } }
方案二:完全自定义令牌请求与响应处理逻辑
若需更灵活的控制,可抛弃RestClientAuthorizationCodeTokenResponseClient,自行实现完整的请求发送与响应解析流程:
@RequiredArgsConstructor public class CustomAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> { private final RestClient restClient = RestClient.create(); private final DatevTokenService datevTokenService; private final UserRepository userRepository; @Override public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpSession session = request.getSession(false); final String codeVerifier = (session != null) ? (String) session.getAttribute("code_verifier") : null; if (codeVerifier == null) { throw new IllegalStateException("code_verifier not found in session"); } // 构建请求参数 CustomTokenRequestParametersConverter paramsConverter = new CustomTokenRequestParametersConverter(codeVerifier); MultiValueMap<String, String> params = paramsConverter.convert(authorizationGrantRequest); // 发送POST请求并直接用自定义转换器解析响应 return restClient.post() .uri(authorizationGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()) .body(params) .retrieve() .body(new ParameterizedTypeReference<OAuth2AccessTokenResponse>() {}, new CustomAccessTokenResponseConverter()); } }
方案对比
- 方案一:复用框架原有逻辑,仅替换响应转换器,适合需保留Spring Security OAuth2默认流程的场景。
- 方案二:完全掌控请求与响应处理,适合需要添加更多定制逻辑(如日志、异常处理)的场景。
- 两种方案均可直接复用你已编写的
CustomAccessTokenResponseConverter,无需修改其核心逻辑。
内容的提问来源于stack exchange,提问作者Kamran Kamilli
相关产品推荐
相关产品推荐

