java - 如何从 Java 进行 Amazon AWS API 调用?

标签 java amazon-web-services rest authorization

如果我想从 Java 调用 Amazon AWS Rest API,我有哪些选择。

在实现我自己的请求时,生成 AWS4-HMAC-SHA256 授权 header 将是最困难的。

本质上,这是我需要生成的 header :

Authorization: AWS4-HMAC-SHA256 Credential=AKIAJTOUYS27JPVRDUYQ/20200602/us-east-1/route53/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ba85affa19fa4a8735ce952e50d41c8c93406a11d22b88cc98b109b529bcc15e

最佳答案

并不是说这是一个完整的列表,但我会考虑使用已建立的库,例如:

  • 官方AWS SDK v1 ,或v2 - 当前且全面,但取决于 netty.io和许多其他 jar 。
  • Apache JClouds - 依赖于 JAXB,它不再是 JDK 的一部分,但现在可以在 Maven 中心单独使用。

但有时,您只想进行一个简单的调用,并且不想将许多依赖项带入您的应用程序。您可能想自己实现其余的调用。生成正确的 AWS 授权 header 是最难实现的部分。

下面是在纯 Java OpenJDK 中执行此操作的代码,没有外部依赖项。

它实现了Amazon AWS API 签名版本 4 签名流程。

AmazonRequestSignatureV4Utils.java
package com.frusal.amazonsig4;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * Copyright 2020 Alex Vasiliev, licensed under the Apache 2.0 license: https://opensource.org/licenses/Apache-2.0
 */
public class AmazonRequestSignatureV4Utils {

    /**
     * Generates signing headers for HTTP request in accordance with Amazon AWS API Signature version 4 process.
     * <p>
     * Following steps outlined here: <a href="https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">docs.aws.amazon.com</a>
     * <p>
     * Simple usage example is here: {@link AmazonRequestSignatureV4Example}
     * <p>
     * This method takes many arguments as read-only, but adds necessary headers to @{code headers} argument, which is a map.
     * The caller should make sure those parameters are copied to the actual request object.
     * <p>
     * The ISO8601 date parameter can be created by making a call to:<br>
     * - {@code java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").format(ZonedDateTime.now(ZoneOffset.UTC))}<br>
     * or, if you prefer joda:<br>
     * - {@code org.joda.time.format.ISODateTimeFormat.basicDateTimeNoMillis().print(DateTime.now().withZone(DateTimeZone.UTC))}
     *
     * @param method - HTTP request method, (GET|POST|DELETE|PUT|...), e.g., {@link java.net.HttpURLConnection#getRequestMethod()}
     * @param host - URL host, e.g., {@link java.net.URL#getHost()}.
     * @param path - URL path, e.g., {@link java.net.URL#getPath()}.
     * @param query - URL query, (parameters in sorted order, see the AWS spec) e.g., {@link java.net.URL#getQuery()}.
     * @param headers - HTTP request header map. This map is going to have entries added to it by this method. Initially populated with
     *     headers to be included in the signature. Like often compulsory 'Host' header. e.g., {@link java.net.HttpURLConnection#getRequestProperties()}.
     * @param body - The binary request body, for requests like POST.
     * @param isoDateTime - The time and date of the request in ISO8601 basic format, see comment above.
     * @param awsIdentity - AWS Identity, e.g., "AKIAJTOUYS27JPVRDUYQ"
     * @param awsSecret - AWS Secret Key, e.g., "I8Q2hY819e+7KzBnkXj66n1GI9piV+0p3dHglAzQ"
     * @param awsRegion - AWS Region, e.g., "us-east-1"
     * @param awsService - AWS Service, e.g., "route53"
     */
    public static void calculateAuthorizationHeaders(
            String method, String host, String path, String query, Map<String, String> headers,
            byte[] body,
            String isoDateTime,
            String awsIdentity, String awsSecret, String awsRegion, String awsService
    ) {
        try {
            String bodySha256 = hex(sha256(body));
            String isoJustDate = isoDateTime.substring(0, 8); // Cut the date portion of a string like '20150830T123600Z';

            headers.put("Host", host);
            headers.put("X-Amz-Content-Sha256", bodySha256);
            headers.put("X-Amz-Date", isoDateTime);

            // (1) https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
            List<String> canonicalRequestLines = new ArrayList<>();
            canonicalRequestLines.add(method);
            canonicalRequestLines.add(path);
            canonicalRequestLines.add(query);
            List<String> hashedHeaders = new ArrayList<>();
            List<String> headerKeysSorted = headers.keySet().stream().sorted(Comparator.comparing(e -> e.toLowerCase(Locale.US))).collect(Collectors.toList());
            for (String key : headerKeysSorted) {
                hashedHeaders.add(key.toLowerCase(Locale.US));
                canonicalRequestLines.add(key.toLowerCase(Locale.US) + ":" + normalizeSpaces(headers.get(key)));
            }
            canonicalRequestLines.add(null); // new line required after headers
            String signedHeaders = hashedHeaders.stream().collect(Collectors.joining(";"));
            canonicalRequestLines.add(signedHeaders);
            canonicalRequestLines.add(bodySha256);
            String canonicalRequestBody = canonicalRequestLines.stream().map(line -> line == null ? "" : line).collect(Collectors.joining("\n"));
            String canonicalRequestHash = hex(sha256(canonicalRequestBody.getBytes(StandardCharsets.UTF_8)));

            // (2) https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
            List<String> stringToSignLines = new ArrayList<>();
            stringToSignLines.add("AWS4-HMAC-SHA256");
            stringToSignLines.add(isoDateTime);
            String credentialScope = isoJustDate + "/" + awsRegion + "/" + awsService + "/aws4_request";
            stringToSignLines.add(credentialScope);
            stringToSignLines.add(canonicalRequestHash);
            String stringToSign = stringToSignLines.stream().collect(Collectors.joining("\n"));

            // (3) https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
            byte[] kDate = hmac(("AWS4" + awsSecret).getBytes(StandardCharsets.UTF_8), isoJustDate);
            byte[] kRegion = hmac(kDate, awsRegion);
            byte[] kService = hmac(kRegion, awsService);
            byte[] kSigning = hmac(kService, "aws4_request");
            String signature = hex(hmac(kSigning, stringToSign));

            String authParameter = "AWS4-HMAC-SHA256 Credential=" + awsIdentity + "/" + credentialScope + ", SignedHeaders=" + signedHeaders + ", Signature=" + signature;
            headers.put("Authorization", authParameter);

        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            } else {
                throw new IllegalStateException(e);
            }
        }
    }

    private static String normalizeSpaces(String value) {
        return value.replaceAll("\\s+", " ").trim();
    }

    public static String hex(byte[] a) {
        StringBuilder sb = new StringBuilder(a.length * 2);
        for(byte b: a) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
     }

    private static byte[] sha256(byte[] bytes) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(bytes);
        return digest.digest();
    }

    public static byte[] hmac(byte[] key, String msg) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
    }

}

以及使用示例:

AmazonRequestSignatureV4Utils.java
package com.frusal.amazonsig4;

import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class AmazonRequestSignatureV4Example {

    public static void main(String[] args) throws Exception {
        String route53HostedZoneId = "Z08118721NNU878C4PBNA";
        String awsIdentity = "AKIAJTOUYS27JPVRDUYQ";
        String awsSecret = "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";
        String awsRegion = "us-east-1";
        String awsService = "route53";

        URL url = new URL("https://route53.amazonaws.com/2013-04-01/hostedzone/" + route53HostedZoneId + "/rrset");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        System.out.println(connection.getRequestMethod() + " " + url);

        String body = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<ChangeResourceRecordSetsRequest xmlns=\"https://route53.amazonaws.com/doc/2013-04-01/\">\n" +
                "<ChangeBatch>\n" +
                // " <Comment>optional comment about the changes in this change batch request</Comment>\n" +
                "   <Changes>\n" +
                "      <Change>\n" +
                "         <Action>UPSERT</Action>\n" +
                "         <ResourceRecordSet>\n" +
                "            <Name>c001cxxx.frusal.com.</Name>\n" +
                "            <Type>A</Type>\n" +
                "            <TTL>300</TTL>\n" +
                "            <ResourceRecords>\n" +
                "               <ResourceRecord>\n" +
                "                  <Value>157.245.232.185</Value>\n" +
                "               </ResourceRecord>\n" +
                "            </ResourceRecords>\n" +
                // " <HealthCheckId>optional ID of a Route 53 health check</HealthCheckId>\n" +
                "         </ResourceRecordSet>\n" +
                "      </Change>\n" +
                "   </Changes>\n" +
                "</ChangeBatch>\n" +
                "</ChangeResourceRecordSetsRequest>";
        byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);

        Map<String, String> headers = new LinkedHashMap<>();
        String isoDate = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").format(ZonedDateTime.now(ZoneOffset.UTC));
        AmazonRequestSignatureV4Utils.calculateAuthorizationHeaders(
                connection.getRequestMethod(),
                connection.getURL().getHost(),
                connection.getURL().getPath(),
                connection.getURL().getQuery(),
                headers,
                bodyBytes,
                isoDate,
                awsIdentity,
                awsSecret,
                awsRegion,
                awsService);

        // Unsigned headers
        headers.put("Content-Type", "text/xml; charset=utf-8"); // I guess it get modified somewhere on the way... Let's just leave it out of the signature.

        // Log headers and body
        System.out.println(headers.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()).collect(Collectors.joining("\n")));
        System.out.println(body);

        // Send
        headers.forEach((key, val) -> connection.setRequestProperty(key, val));
        connection.setDoOutput(true);
        connection.getOutputStream().write(bodyBytes);
        connection.getOutputStream().flush();

        int responseCode = connection.getResponseCode();
        System.out.println("connection.getResponseCode()=" + responseCode);

        String responseContentType = connection.getHeaderField("Content-Type");
        System.out.println("responseContentType=" + responseContentType);

        System.out.println("Response BODY:");
        if (connection.getErrorStream() != null) {
            System.out.println(new String(connection.getErrorStream().readAllBytes(), StandardCharsets.UTF_8));
        } else {
            System.out.println(new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
        }
    }
}
以及它会生成的跟踪:
POST https://route53.amazonaws.com/2013-04-01/hostedzone/Z08118721NNU878C4PBNA/rrset
Host: route53.amazonaws.com
X-Amz-Content-Sha256: 46c7521da55bcf9e99fa6e12ec83997fab53128b5df0fb12018a6b76fb2bf891
X-Amz-Date: 20200602T035618Z
Authorization: AWS4-HMAC-SHA256 Credential=AKIAJTOUYS27JPVRDUYQ/20200602/us-east-1/route53/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=6a59090f837cf71fa228d2650e9b82e9769e0ec13e9864e40bd2f81c682ef8cb
Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?>
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<ChangeBatch>
   <Changes>
      <Change>
         <Action>UPSERT</Action>
         <ResourceRecordSet>
            <Name>c001cxxx.frusal.com.</Name>
            <Type>A</Type>
            <TTL>300</TTL>
            <ResourceRecords>
               <ResourceRecord>
                  <Value>157.245.232.185</Value>
               </ResourceRecord>
            </ResourceRecords>
         </ResourceRecordSet>
      </Change>
   </Changes>
</ChangeBatch>
</ChangeResourceRecordSetsRequest>
connection.getResponseCode()=200
responseContentType=text/xml
Response BODY:
<?xml version="1.0"?>
<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/"><ChangeInfo><Id>/change/C011827119UYGF04GVIP6</Id><Status>PENDING</Status><SubmittedAt>2020-06-02T03:56:25.822Z</SubmittedAt></ChangeInfo></ChangeResourceRecordSetsResponse>

编辑:更新以对标题进行排序。感谢@Gray 的发现!

有关此代码的最新版本,请参阅java-amazon-request-signature-v4 GitHub 上的存储库。

关于java - 如何从 Java 进行 Amazon AWS API 调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62144379/

相关文章:

java - 如何获取从缓冲读取器文件中获得的数字?

java - 如何使用 ElGamal 加密/解密文本文件

java - 如何从用于 Java 的谷歌应用引擎调用谷歌应用脚​​本

amazon-web-services - cloudformation部署capability_auto_expand

C# 将 40 个字节添加到流中

Http Session 对象的 C# 到 Java

amazon-web-services - AWS Lambda - 使用 .NET Core 构建无服务器 API

python - boto3 搜索未使用的安全组

zend-framework - 使用 Zend_http_Client 获取响应主体

REST - 删除后使用 HTTP 缓存获取集合