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

Spring中使用WebClient获取Client Credentials模式OAuth2令牌返回401 Unauthorized,Postman请求正常的问题排查

Spring中使用WebClient获取Client Credentials模式OAuth2令牌返回401 Unauthorized,Postman请求正常的问题排查

遇到这种Postman能正常拿到令牌但Spring代码返回401的情况,基本都是请求细节和Postman的配置不一致导致的,咱们先从最核心的差异点入手排查:

1. 客户端凭证传递方式不匹配(最可能的原因)

你在Postman里明确设置了 Client Authentication: Send as Basic Auth headers,这意味着OAuth2服务端要求客户端通过HTTP Basic认证头来传递client_id和client_secret,而不是把这两个参数放到表单(form data)里。

但你的代码里是把client_id和client_secret直接加到了formData中,这就导致服务端无法识别你的客户端身份,直接返回401 Unauthorized。

修复代码的方法:

  • 移除formData中的client_id和client_secret参数
  • 给WebClient请求添加Basic Auth认证头,将client_id和client_secret拼接后做Base64编码

修改后的代码片段如下:

private OAuth2AuthorizedClient obtainToken(ClientRegistration clientRegistration) {
    String tokenUrl = clientRegistration.getProviderDetails().getTokenUri();
    String clientId = clientRegistration.getClientId();
    String clientSecret = clientRegistration.getClientSecret();
    String scope = "https://test.account.myconpamys/auth/gpr/.default";

    // 生成Basic Auth凭证:clientId:clientSecret 做Base64编码
    String basicAuthCredentials = Base64.getEncoder()
            .encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));

    WebClient webClient = WebClient.builder().build();

    // 准备form数据,移除client_id和client_secret
    MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
    formData.add("grant_type", "client_credentials");
    formData.add("scope", scope); // 只保留grant_type和scope

    try {
        Map<String, Object> responseBody = webClient.post()
                .uri(tokenUrl)
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                // 添加Basic Auth请求头
                .header(HttpHeaders.AUTHORIZATION, "Basic " + basicAuthCredentials)
                .body(BodyInserters.fromFormData(formData))
                .retrieve()
                .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
                .block();

        // 后续的令牌解析逻辑保持不变...
        if (responseBody == null || !responseBody.containsKey("access_token")) {
            if (responseBody != null && responseBody.containsKey("token")) {
                responseBody = (Map<String, Object>) responseBody.get("token");
            }
            if (responseBody == null || !responseBody.containsKey("access_token")) {
                throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token"), "Failed to obtain access token");
            }
        }

        String accessTokenValue = (String) responseBody.get("access_token");
        String tokenType = (String) responseBody.get("token_type");
        int expiresIn = (Integer) responseBody.get("expires_in");
        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt.plusSeconds(expiresIn);

        OAuth2AccessToken accessToken = new OAuth2AccessToken(
                OAuth2AccessToken.TokenType.BEARER,
                accessTokenValue,
                issuedAt,
                expiresAt,
                clientRegistration.getScopes()
        );

        return new OAuth2AuthorizedClient(clientRegistration, "my-client", accessToken);
    } catch (Exception e) {
        throw new OAuth2AuthenticationException(new OAuth2Error("invalid_token"), 
        "Failed to obtain access token: " + e.getMessage(), e);
    }
}

2. 其他可能的排查点

如果修改后还是报错,可以检查以下细节:

  • Scope完全匹配:确认代码里的scope和Postman中填写的完全一致,包括拼写、大小写、斜杠等细节(比如有没有多打/少打字符)
  • Token URL正确性:对比代码中的tokenUrl和Postman里的Access Token URL,确保没有拼写错误或者多余的路径
  • 字符编码问题:确保Basic Auth的Base64编码使用的是UTF-8,避免特殊字符(比如client_secret里的特殊符号)编码错误

备注:内容来源于stack exchange,提问作者user2557930

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 09:14:33