如何从Java调用Amazon AWS API?

qmelpv7a  于 2023-03-06  发布在  Java
关注(0)|答案(2)|浏览(231)

如果我想从Java调用Amazon AWS Rest API,我有哪些选择。
在实现我自己的请求时,生成AWS4-HMAC-SHA256 Authorization头将是最困难的。
实际上,这就是我需要生成的头文件:

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

zbwhf8kr1#

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

  • 正式的AWS SDK v1v2-当前和全面的,但取决于netty.io和许多其他jar。
  • Apache JClouds-依赖于JAXB,JAXB不再是JDK的一部分,但现在可以在maven central单独获得。

但有时候,您只需要进行一个简单的调用,而不希望在应用程序中引入许多依赖项。您可能希望自己实现其余调用。生成正确的AWS Authorization头是最难实现的部分。
下面是在没有外部依赖的纯java OpenJDK中执行此操作的代码。
它实现了Amazon AWS API签名版本4签名流程。
AmazonRequestSignatureV4Utils.java 例如
使用示例:
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的发现!
有关此代码的最新版本,请参见GitHub上的java-amazon-request-signature-v4仓库。

vwkv1x7d

vwkv1x7d2#

如何从Java调用Amazon AWS API?
我从@AlexV得到了答案,然后修复了代码中的一些问题。(大小写不敏感)否则签名将不匹配。我想我可能修复了一些其他问题?我还通过删除不必要的集合和优化hex和trim方法改进了一些其他方法。我还更改了calculate方法以返回链接哈希-标头的Map,以便可以将它们适当地放入请求中。
这段代码与AWS签名文档示例相匹配(最后),我已经成功地使用S3验证了请求。

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>
     * The ISO8601 date parameter should look like '20150830T123600Z' and can be created by making a call to:
     * 
     * <pre>
     * java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").format(ZonedDateTime.now(ZoneOffset.UTC))}
     * </pre>
     * 
     * or, if you prefer joda:
     * 
     * <pre>
     * {@code org.joda.time.format.ISODateTimeFormat.basicDateTimeNoMillis().print(DateTime.now().withZone(DateTimeZone.UTC))}
     * </pre>
     *
     * @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 headerMap
     *            HTTP request header map. The returned map should be used in the request.
     * @param body
     *            The binary request body, for requests like POST or PUT. If null then new byte[0] will be used.
     * @param isoDateTime
     *            The time and date of the request in ISO8601 basic format like '20150830T123600Z', see comment above.
     * @param awsAccessKeyId
     *            AWS Identity, e.g., "AKIAJTOUY..."
     * @param awsSecretAccessKey
     *            AWS Secret Key, e.g., "I8Q2hY819e+7KzB..."
     * @param awsRegion
     *            AWS Region, e.g., "us-east-1"
     * @param awsService
     *            AWS Service, e.g., "route53"
     * @return Linked map of properties that should be set on the http request with no other headers.
     */
    public static Map<String, String> calculateAuthorizationHeaders(String method, String host, String path,
            String query, Map<String, String> headerMap, byte[] body, String isoDateTime, String awsAccessKeyId,
            String awsSecretAccessKey, String awsRegion, String awsService)
            throws GeneralSecurityException {

        if (body == null) {
            body = new byte[0];
        }
        String bodySha256 = hex(sha256(body));
        String isoJustDate = isoDateTime.substring(0, 8); // Cut the date portion of a string like '20150830T123600Z';

        Map<String, String> resultHeaderMap = new LinkedHashMap<>();

        // (1) https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
        StringBuilder canonicalRequestSb = new StringBuilder();
        appendOptionalString(canonicalRequestSb, method);
        appendOptionalString(canonicalRequestSb, path);
        appendOptionalString(canonicalRequestSb, query);

        final String HOST_HEADER = "Host";
        final String AMAZON_DATE_HEADER = "X-Amz-Date";
        final String AMAZON_CONTENT_HASH_HEADER = "X-Amz-Content-Sha256";

        // need to order the headers to match the AWS docs but may not be necessary for the request
        List<String> headerList = new ArrayList<>(headerMap.keySet());
        headerList.add(HOST_HEADER);
        headerList.add(AMAZON_CONTENT_HASH_HEADER);
        headerList.add(AMAZON_DATE_HEADER);
        Collections.sort(headerList, String.CASE_INSENSITIVE_ORDER);
        StringBuilder hashedHeadersSb = new StringBuilder();
        for (String header : headerList) {
            String value = headerMap.get(header);
            if (header.equals(HOST_HEADER)) {
                value = host;
            } else if (header.equals(AMAZON_CONTENT_HASH_HEADER)) {
                value = bodySha256;
            } else if (header.equals(AMAZON_DATE_HEADER)) {
                value = isoDateTime;
            }
            resultHeaderMap.put(header, value);
            String headerLower = header.toLowerCase();
            if (hashedHeadersSb.length() > 0) {
                hashedHeadersSb.append(';');
            }
            hashedHeadersSb.append(headerLower);
            String cannonicalValue = headerLower + ":" + normalizeSpaces(value);
            appendOptionalString(canonicalRequestSb, cannonicalValue);
        }
        canonicalRequestSb.append('\n'); // new line required after headers

        // names of the headers, lowercase, separated by ';'
        String signedHeaders = hashedHeadersSb.toString();
        appendOptionalString(canonicalRequestSb, signedHeaders);
        appendOptionalString(canonicalRequestSb, bodySha256);
        // trim off the last newline
        canonicalRequestSb.setLength(canonicalRequestSb.length() - 1);
        String canonicalRequestBody = canonicalRequestSb.toString();
        String canonicalRequestHash = hex(sha256(canonicalRequestBody.getBytes(StandardCharsets.UTF_8)));

        final String AWS_ENCRYPTION_ALGORITHM = "AWS4-HMAC-SHA256";

        // (2) https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
        StringBuilder stringToSignSb = new StringBuilder();
        stringToSignSb.append(AWS_ENCRYPTION_ALGORITHM).append('\n');
        stringToSignSb.append(isoDateTime).append('\n');
        String requestType = "aws4_request";
        String credentialScope = isoJustDate + "/" + awsRegion + "/" + awsService + "/" + requestType;
        stringToSignSb.append(credentialScope).append('\n');
        stringToSignSb.append(canonicalRequestHash);
        String stringToSign = stringToSignSb.toString();

        // (3) https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
        byte[] keySecret = ("AWS4" + awsSecretAccessKey).getBytes(StandardCharsets.UTF_8);
        byte[] keyDate = hmac(keySecret, isoJustDate);
        byte[] keyRegion = hmac(keyDate, awsRegion);
        byte[] keyService = hmac(keyRegion, awsService);
        byte[] keySigning = hmac(keyService, requestType);
        String signature = hex(hmac(keySigning, stringToSign));

        // finally we can build our Authorization header
        String authParameter = AWS_ENCRYPTION_ALGORITHM + " Credential=" + awsAccessKeyId + "/" + credentialScope
                + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
        resultHeaderMap.put("Authorization", authParameter);
        return resultHeaderMap;
    }

    private static void appendOptionalString(StringBuilder sb, String str) {
        if (str != null) {
            sb.append(str);
        }
        sb.append('\n');
    }

    /** replaces multiple whitespace character blocks into a single space, removes whitespace from the front and end */
    private static String normalizeSpaces(String value) {
        StringBuilder sb = new StringBuilder(value.length());
        boolean prevSpace = false;
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            if (Character.isWhitespace(ch)) {
                if (sb.length() == 0) {
                    // skip spaces at the start
                } else {
                    // note that we saw a space which reduces multiples and ignores spaces
                    // this filters whitespace at the end of the string because they only emitted before the next char
                    prevSpace = true;
                }
                continue;
            }
            if (prevSpace) {
                // this changes all whitespace into a space
                sb.append(' ');
                prevSpace = false;
            }
            sb.append(ch);
        }
        return sb.toString();
    }

    private static String hex(byte[] bytes) {
        final String HEX_CHARACTERS = "0123456789abcdef";
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (int i = 0; i < bytes.length; i++) {
            int val = (bytes[i] & 0xFF);
            sb.append(HEX_CHARACTERS.charAt(val >>> 4));
            sb.append(HEX_CHARACTERS.charAt(val & 0x0F));
        }
        return sb.toString();
    }

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

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

下面是一个使用示例,它显示了使用Apache HttpClient 4.5.13版发出的实际HTTP请求:

String method = "GET";
String host = "somebucket.s3.amazonaws.com";
String path = "/somepath.txt";
String query = null;
// probably should come from a file or something if doing a PUT or POST
String body = null;
String awsIdentity = "AKIA...";
String awsSecret = "s1Zpk...";
String awsRegion = "us-west-2";
String awsService = "s3";

// optional headers
Map<String, String> headers = new HashMap<>();

ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC);
String dateHeader = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'UTC'").format(zonedDateTime);
headers.put("Date", dateHeader);
String isoDateTime = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").format(zonedDateTime);

// this is a linked hashmap and the headers must be in a specific order
Map<String, String> requestHeaders =
            AmazonRequestSignatureV4Utils.calculateAuthorizationHeaders(method, host, path, query, headers,
                    body.getBytes(), isoDateTime, awsIdentity, awsSecret, awsRegion, awsService);
try (CloseableHttpClient httpClient = HttpClients.createDefault();) {
    HttpGet request = new HttpGet("https://" + host + path);
    List<Header> finalHeaders = new ArrayList<>(requestHeaders.size());
    // these must be in a specific order
    for (Entry<String, String> entry : requestHeaders.entrySet()) {
        finalHeaders.add(new BasicHeader(entry.getKey(), entry.getValue()));
    }
    // this overrides any previous headers specified
    request.setHeaders(finalHeaders.toArray(new Header[requestHeaders.size()]));
    try (CloseableHttpResponse response = httpClient.execute(request);) {
        System.out.println("response status: " + response.getStatusLine());
        System.out.println("response body:");
        System.out.println(IOUtils.toString(response.getEntity().getContent()));
    }
}

相关问题