如何在 Java 中使用 API KEY 身份验证正确签署对 Coinbase V2 API 的请求

How to properly sign requests to Coinbase V2 API using API KEY Authentication in Java

我正在为 coinbase v2 API 开发一个 java 包装器,因为我找不到任何按我想要的方式工作的库:以“流畅”的方式,使用 vavr 数据类型 & api密钥认证。

首先,我开发了 coinbase API 的整个 public 数据包装器,它运行良好。它允许我调用 Coinbase 货币、汇率、价格...

现在我想调用 https://api.coinbase.com/v2/user 之类的安全端点。 Si 我遵循 https://developers.coinbase.com/docs/wallet/api-key-authentication

中定义的规范

到目前为止没有什么疯狂的,我只是遵循指导方针,即使没有基于 java 但在 python、ruby 和 nodejs 上它看起来并不难。 .. 但是当我执行对端点的调用时,例如:https://api.coinbase.com/v2/user 我得到这个响应:

"errors":[
  {
    "id": "authentication_error",
    "message": "invalid signature"
  }
]

所以我决定进行一些研究,因为我认为我的签名过程有误。所以我找到了很多非官方的 java & C# wrappers 并观察了它们是如何做的,它看起来很像我在我的代码中所做的......但没关系,我采用了不同的代码我找到并根据我的情况调整它们,以便它们适合我的代码。然后,在实施了 6 种不同的方式来签署我的请求之后,它仍然无法正常工作。

我有六种不同的无效方法来为 coinbase api 密钥身份验证 API :

    public String[] getAuthenticationHeaders(
      final String apiKey,
      final String secret,
      final long timestamp,
      final String httpMethod,
      final String httpPath,
      final String httpBody) {

    if (StringUtils.isBlank(apiKey) || StringUtils.isBlank(secret)) {
      manageNotAllowed(
          new JCoinbaseException(
              "You must specify an Api key and a secret to access this resource."));
    }

    //SIGNATURE VERSION 1
    var message = timestamp + httpMethod + httpPath + ((httpBody == null) ? "" : httpBody);
    var signature1 = new HmacUtils(HMAC_SHA_256, secret.getBytes()).hmacHex(message);

    //SIGNATURE VERSION 2
    var signature2 = HmacUtils.hmacSha256Hex(secret, message);

    //SIGNATURE VERSION 3
    var mac =
        HmacUtils.getInitializedMac("HmacSHA256", secret.getBytes(StandardCharsets.UTF_8));
    mac.update(message.getBytes(StandardCharsets.UTF_8));
    var signature3 = String.format("%064x", new BigInteger(1, mac.doFinal()));

    //SIGNATURE VERSION 4
    String signature4 = "";
    try {
      String prehash = timestamp + httpMethod.toUpperCase() + httpPath + httpBody;
      byte[] secretDecoded = Base64.getDecoder().decode(secret);
      SecretKeySpec keyspec = new SecretKeySpec(secretDecoded, Mac.getInstance("HmacSHA256").getAlgorithm());
      Mac sha256 = (Mac) Mac.getInstance("HmacSHA256");
      sha256.init(keyspec);
      signature4 = Base64.getEncoder().encodeToString(sha256.doFinal(prehash.getBytes()));
    } catch (InvalidKeyException | NoSuchAlgorithmException e) {
      e.printStackTrace();
      throw new RuntimeException(new Error("Cannot set up authentication headers."));
    }

    //SIGNATURE VERSION 5
    var hmacKey = secret.getBytes(StandardCharsets.UTF_8);
    var messageBytes = message.getBytes(StandardCharsets.UTF_8);
    var hmac = HmacUtils.getInitializedMac("HmacSHA256", hmacKey);
    var sig = hmac.doFinal(messageBytes);
    char[] c = new char[sig.length * 2];
    int b;
    for(int i = 0; i < sig.length; i++){
      b = sig[i] >> 4;
      c[i * 2] = (char)(87 + b + (((b - 10) >> 31) & -39));
      b = sig[i] & 0xF;
      c[i * 2 + 1] = (char)(87 + b + (((b - 10) >> 31) & -39));
    }
    var signature5 = String.valueOf(c);

    //SIGNATURE VERSION 6
    var preHash = timestamp + httpMethod.toUpperCase() + httpPath + httpBody;
    byte[] secretDecoded = Base64.getDecoder().decode(secret);
    SecretKeySpec keySpec;
    Mac sha256 = null;
    try {
      keySpec = new SecretKeySpec(secretDecoded, Mac.getInstance("HmacSHA256").getAlgorithm());
      sha256 = Mac.getInstance("HmacSHA256");
      sha256.init(keySpec);
    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
      e.printStackTrace();
    }
    var signature6 = Base64.getEncoder().encodeToString(sha256.doFinal(preHash.getBytes()));


    return new String[] {
      "CB-ACCESS-SIGN",
      signature6,
      "CB-ACCESS-TIMESTAMP",
      String.valueOf(timestamp),
      "CB-ACCESS-KEY",
      apiKey,
      "Accept",
      "application/json"
    };
  }

如你所见,我真的试过了,但还是不行,所以我请求你帮助我,拜托:)

为了更好地理解,这里是使用此签名过程的包装器的不同简化代码片段:

//A RANDOM TESTING CLASS FOR DEVELOPMENT PURPOSE ONLY :)
@Test
void main() {
  JCoinbaseClient client = JCoinbaseClientFactory.build("myApiKey", "myApiSecret");
  var currentUser = client.user().fetchCurrentUser();
}

//JCoinbaseClient class
public UserService user() {
  var allowed = authService.allow(this);

  if (allowed.isLeft()) {
    manageNotAllowed(allowed.getLeft());
  }
   return userService;
}

//UserService class
public User fetchCurrentUser() {
  return service
      .fetchCurrentUser(client, authentication)
      .onSuccess(user -> log.info("Successfully fetch current user."))
      .onFailure(
          throwable ->
              manageOnFailure(
                  new JCoinbaseException(throwable),
                  "An error occurred while fetching current user",
                  throwable))
      .get();
}

//CoinbaseUserService class
public Try<User> fetchCurrentUser(final JCoinbaseClient client, final AuthenticationService authentication) {

  var requestHeaders =
      authentication.getAuthenticationHeaders(
          client.getProperties(), "GET", client.getProperties().getUserPath(), "");

  var request =
      HttpRequest.newBuilder()
          .GET()
          .uri(
              URI.create(
                  client.getProperties().getApiUrl() + client.getProperties().getUserPath()))
          .headers(requestHeaders)
          .build();

  return Try.of(() -> client.getClient().send(request, BodyHandlers.ofString()))
      .mapTry(
          stringHttpResponse ->
              client
                  .getJsonSerDes()
                  .readValue(stringHttpResponse.body(), UserDto.class)
                  .toUser());
}

//AuthenticationService class
public String[] getAuthenticationHeaders(
    final JCoinbaseProperties properties,
    final String httpMethod,
    final String httpPath,
    final String httpBody) {
  return getAuthenticationHeaders(
      properties.getApiKey().getOrNull(),
      properties.getSecret().getOrNull(),
      getCurrentTime(),
      httpMethod,
      httpPath,
      httpBody);
}

public String[] getAuthenticationHeaders(
      final String apiKey,
      final String secret,
      final long timestamp,
      final String httpMethod,
      final String httpPath,
      final String httpBody) {
   // YOU CAN FIND THIS CODE IN THE FIRST CODE SNIPPET
}

最后,我的控制台中的结果(http 错误处理尚未开发。这是假定的)

com.github.badpop.jcoinbase.exception.JCoinbaseException: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "errors" (class com.github.badpop.jcoinbase.client.service.user.dto.UserDto), not marked as ignorable (15 known properties: "bitcoin_unit", "avatar_url", "created_at", "name", "resource", "profile_bio", "username", "profile_url", "id", "email", "time_zone", "profile_location", "resource_path", "native_currency", "country"])
 at [Source: (String)"{"errors":[{"id":"authentication_error","message":"invalid signature"}]}"; line: 1, column: 73] (through reference chain: com.github.badpop.jcoinbase.client.service.user.dto.UserDto["errors"])

    at com.github.badpop.jcoinbase.client.service.user.UserService.lambda$fetchCurrentUser(UserService.java:26)
    at io.vavr.control.Try.onFailure(Try.java:659)
    at com.github.badpop.jcoinbase.client.service.user.UserService.fetchCurrentUser(UserService.java:24)
    at com.github.badpop.jcoinbase.client.service.user.UserServiceTest.main(UserServiceTest.java:59)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod[=16=](ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke[=16=](ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute[=16=](EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "errors" (class com.github.badpop.jcoinbase.client.service.user.dto.UserDto), not marked as ignorable (15 known properties: "bitcoin_unit", "avatar_url", "created_at", "name", "resource", "profile_bio", "username", "profile_url", "id", "email", "time_zone", "profile_location", "resource_path", "native_currency", "country"])
 at [Source: (String)"{"errors":[{"id":"authentication_error","message":"invalid signature"}]}"; line: 1, column: 73] (through reference chain: com.github.badpop.jcoinbase.client.service.user.dto.UserDto["errors"])
    at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:987)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1974)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1686)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperties(BeanDeserializerBase.java:1635)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:541)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1390)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)
    at com.github.badpop.jcoinbase.client.service.user.CoinbaseUserService.lambda$fetchCurrentUser$faa3259(CoinbaseUserService.java:37)
    at io.vavr.control.Try.mapTry(Try.java:634)
    at com.github.badpop.jcoinbase.client.service.user.CoinbaseUserService.fetchCurrentUser(CoinbaseUserService.java:33)
    at com.github.badpop.jcoinbase.client.service.user.UserService.fetchCurrentUser(UserService.java:22)
    ... 66 more

有什么想法,我采纳!

注意:如果您愿意,可以在 github 上访问包装器代码:https://github.com/Bad-Pop/JCoinbasefeat/fetch-user-resources git 分支

您编码的 requestPath 只是 /user 而不是 /v2/user

根据链接的规范:

The requestPath is the full path and query parameters of the URL, e.g.: /v2/exchange-rates?currency=USD.

根据你的来源

//CoinbaseUserService.java:
public Try<User> fetchCurrentUser(final JCoinbaseClient client, final AuthenticationService authentication) {
    var requestHeaders = authentication.getAuthenticationHeaders(
        client.getProperties(), "GET",      client.getProperties().getUserPath()     , "");
    // ...

//JCoinbaseProperties.java:
private void extractProperties(final String apiKey, final String secret) { /*...*/
    this.userPath = properties.getProperty(    "coinbase.api.path.resource.user"    ); // ...

//jcoinbase.properties:
    coinbase.api.path.resource.user=/user