You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:22:07