使用客户端凭据授权从具有 Java HttpClient 的服务器检索访问令牌

Retrieve access token from server with Java HttpClient using Client Credentials grant

我正在尝试使用戴尔保修 API。为此,您首先需要获取访问令牌(1 小时后过期),然后使用该令牌发出 API 请求。他们有一个邮递员教程,效果很好,但我正在尝试做一些更自动化的事情,因为我们有 1000 多个资产要用这个 API.

查找

我正在尝试使用 java.net.http,尽管旧的 API 有很多示例,我不想使用外部库或更旧的 API s.

要获得令牌,请使用您的客户端 ID 和客户端密码向他们的 API 发送请求。

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.Base64;

public class Main {

    public static void main(String[] args) throws IOException, InterruptedException {
        
        String tokenURL = "https://apigtwb2c.us.dell.com/auth/oauth/v2/token";
        String clientID = "{redacted}";
        String clientSecret = "{redacted}";
        String formatted = clientID + ":" + clientSecret;
        String encoded = Base64.getEncoder().encodeToString((formatted).getBytes());
        
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).followRedirects(Redirect.NORMAL).connectTimeout(Duration.ofSeconds(10)).build();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(tokenURL)).headers("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8", "Accept", "application/json", "grant_type", "client_credentials", "Authorization", "Basic " + encoded)
                .POST(BodyPublishers.noBody()).build();
        
        System.out.println(request.headers());
        
        HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
        
        System.out.println(response.statusCode());
        System.out.println(response.body());
    }

}

服务器正在响应

400
{
  "error":"invalid_request",
  "error_description":"Missing or duplicate parameters"
}

根据他们的文档,400 是一个错误的请求。我目前正在发送一个空的 body,但我不确定这是否是问题所在。

而且我不完全确定缺少什么。我尝试了 headers 的多种组合,但均未成功。这同样适用于 cURL,这正是我走到现在的原因。

这里是 cURL 请求以防万一..

curl -v https://apigtwb2c.us.dell.com/auth/oauth/v2/token -H "Accept: application/json" -u "{redacted}:{redacted}" -d "grant_type=client_credentials"
*   Trying 143.166.28.87:443...
* Connected to apigtwb2c.us.dell.com (143.166.28.87) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=US; ST=Texas; L=Round Rock; O=Dell; CN=*.apis.dell.com
*  start date: Jul 26 19:18:16 2021 GMT
*  expire date: Jul 20 19:18:15 2022 GMT
*  subjectAltName: host "apigtwb2c.us.dell.com" matched cert's "apigtwb2c.us.dell.com"
*  issuer: C=US; O=Entrust, Inc.; OU=See www.entrust.net/legal-terms; OU=(c) 2012 Entrust, Inc. - for authorized use only; CN=Entrust Certification Authority - L1K
*  SSL certificate verify ok.
* Server auth using Basic with user '{redacted}'
> POST /auth/oauth/v2/token HTTP/1.1
> Host: apigtwb2c.us.dell.com
> Authorization: Basic {redacted}
> User-Agent: curl/7.79.1
> Accept: application/json
> Content-Length: 29
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Pragma: no-cache
< Cache-Control: no-store
< X-Correlation-ID: {redacted}
< Content-Type: application/json;charset=UTF-8
< Content-Length: 127
< Date: Thu, 14 Oct 2021 18:56:28 GMT
< Server: dell
<
{
  "access_token":"{redacted}",
  "token_type":"Bearer",
  "expires_in":3600,
  "scope":"oob"
* Connection #0 to host apigtwb2c.us.dell.com left intact

谢谢!

Body 是必需的

根据你的 curl 日志,请求应该有这个 content-type application/x-www-form-urlencoded

这种 content-type 需要一个 body,所以这是你的 第一个错误:

.POST(BodyPublishers.noBody())

第二个错误是在oauth2协议中,grant_type=client_credentials不是header,它是body中的一个表单参数。此外,您的 curl 片段确认:curl ... -d "grant_type=client_credentials"。检查这个:curl -d

客户端凭据授予

如果戴尔平台严格执行oauth2协议,他们应该执行oauth2规范:

https://datatracker.ietf.org/doc/html/rfc6749#section-4.4

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

其中我们可以看到body只需要:

  • application/x-www-form-urlencoded风格的grant_type(不是json)
  • content-type header
  • 授权header 应该像你的String encoded = Base64 ...

如果 Dell 实施了另一种补助金,您应该阅读文档,如下所示:

https://www.oauth.com/oauth2-servers/access-tokens/client-credentials/

POST /token HTTP/1.1
Host: authorization-server.com
 
grant_type=client_credentials
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx

其中我们可以看到 body 需要 client_id 和 client_secret,这与将凭据作为基本身份验证发送的规范不同 header.

Java 请求 x-www-form-urlencoded body

如果戴尔实施此 spec 并假设您使用 Java 11,则此未经测试的代码应该有效:

String url = "https://apigtwb2c.us.dell.com/auth/oauth/v2/token";
Map<String, String> parameters = new HashMap<>();
parameters.put("grant_type", "client_credentials");
parameters.put("client_id", "****");
parameters.put("client_secret", "****");

String form = parameters.keySet().stream()
.map(key -> key + "=" + URLEncoder.encode(parameters.get(key), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url))
.headers("Content-Type", "application/x-www-form-urlencoded")
.POST(BodyPublishers.ofString(form)).build();

HttpResponse<?> response = client.send(request, BodyHandlers.ofString());
System.out.println(response.statusCode() + response.body().toString());

一些供应商支持多种类型的请求,所以前面的代码片段应该可以工作。如果戴尔严格执行主要spec,只需删除这部分:

parameters.put("client_id", "****");
parameters.put("client_secret", "****");

并添加您的授权 header

String formatted = clientID + ":" + clientSecret;
String encoded = Base64.getEncoder().encodeToString((formatted).getBytes());
...
.headers("Content-Type", "application/x-www-form-urlencoded", "Authorization", "Basic " + encoded)

如果以后有人遇到这个问题,JRichardsz 的回答非常有效,但是如果你出于任何原因想避免使用 HashMap:

HttpRequest request = HttpRequest.newBuilder().uri(URI.create(tokenURL)).headers("Accept", "application/json", "Content-Type", "application/x-www-form-urlencoded")
                .POST(BodyPublishers.ofString("grant_type=client_credentials&client_id=" + clientID + "&client_secret=" + clientSecret)).build();