Generieren Sie AWS Signature V4 in Java und fordern Sie die API an

Einführung

Die AWS-API kann die IAM-Authentifizierung nutzen, indem sie die Anforderung mit AWS Signature V4 signiert. Normalerweise müssen Sie die Signature V4-Spezifikationen mithilfe des AWS SDK nicht genau kennen, aber aus verschiedenen Gründen hatte ich die Möglichkeit, Signature V4 selbst zu implementieren, sodass ich eine Notiz hinterlassen werde.

Der in Java 11 implementierte HTTP-Client verwendet den Apache HttpComponents Client (https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient).

Da der Code ausgeschnitten, eingefügt und verarbeitet wird, ist es ein wenig zweifelhaft, ob er ordnungsgemäß funktioniert.

Anfrage, wenn nicht unterschrieben

Aus Gründen der Übersichtlichkeit finden Sie hier eine Beispielanforderung, wenn Sie nicht unterschreiben. Dies ist eine Beispielsuchanforderung unter Verwendung der Elasticsearch-API. Da dieses Beispiel die POST-Methode verwendet, werden die Abfrageparameter in den HTTP-Body geschrieben.

Wir werden diese Anforderung mit AWS Signature V4 signieren und so ändern, dass nur APIs von authentifizierten Benutzern empfangen werden.

import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;

void reqFunc(){
    //Anforderungsgenerierung
    URI uri                 = new URI(AWS_ELASTICSEARCH_URL + "/index/hoge/_search?");
	HttpPost post           = new HttpPost(uri);
	post.setHeader("Content-Type", "application/json");
	post.setEntity(new StringEntity( "Abfragezeichenfolge(Kürzung)", "UTF-8"));

    //Ausführen einer Anforderung mit einem HTTP-Client
    HttpClient client       = HttpClientBuilder.create().build();
    CloseableHttpResponse res = (CloseableHttpResponse) client.execute(post);

    // ...Verarbeitung zur Antwort...
}

Informationen zu Unterschriften

AWS Signature V4 ist auf der Amazon-Website verfügbar. Ich habe die Informationen erhalten, indem ich der unten stehenden URL gefolgt bin. https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

Darüber hinaus erhalten Sie Informationen zum Abrufen der für die Authentifizierung erforderlichen Zugriffsschlüssel-ID und des geheimen Zugriffsschlüssels unter der folgenden URL. https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-sec-cred-types.html

Zeichenimplementierung

AWS Signature V4 generiert einen Hash unter Verwendung der Anforderungsinformationen selbst und der zuvor ausgegebenen Zugriffsschlüssel-ID / des geheimen Zugriffsschlüssels und hängt ihn an den Anforderungsheader an. Ich habe immer und immer wieder gehasht, und ich wusste nicht, wie weit das Ziel des Hashing war, also habe ich einen Versuch und Irrtum gemacht.

Erstellen der HttpRequestInterceptor-Klasse

Bereiten Sie zunächst die HttpRequestInterceptor-Klasse und den HTTP-Client vor, der sie einschließt, um die Informationen der zu sendenden Anforderung selbst abzurufen. Durch Einfügen dieser Klasse beim Generieren eines HTTP-Clients können Sie die Verarbeitung unmittelbar vor dem Senden einer Anforderung an den Server einfügen.

import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.protocol.HttpContext;

public class AmazonizeInterceptor implements HttpRequestInterceptor {

	@Override
	public void process(HttpRequest request, HttpContext context) throws Exception{
        AwsSigner4(request);  
	}

    private void AwsSigner4(HttpRequest request) throws Exception {
        /*Implementieren Sie AWS Signature V4 in dieser Funktionsimplementierung.*/
    }
}
//...Abkürzung...

void reqFunc(){
    //...Abkürzung...

    //Ausführen einer Anforderung mit einem HTTP-Client
    HttpClient client = HttpClientBuilder.create()
                        .addInterceptorLast(new AmazonizeInterceptor())  //← Fügen Sie dies hinzu
                        .build();
    CloseableHttpResponse res = (CloseableHttpResponse) client.execute(post);

    //...Verarbeitung zur Antwort...
}

Wenn Sie jetzt eine Anfrage mit einem HTTP-Client stellen, der RequestInterceptor einschließt, wird die zuvor definierte AwsSigner4-Funktion ausgeführt, bevor sie an den Server gesendet wird.

Implementierung der AwsSigner4-Funktion

Als Nächstes implementieren wir die AwsSigner4-Funktion, die den eigentlichen Signaturprozess ausführt. Alles, was Sie tun müssen, ist, "X-Amz-Date" und "Authorization" zum Anforderungsheader hinzuzufügen. Es sind jedoch mehrere Schritte erforderlich, um den Wert von Authorization zu ermitteln.

  1. Generierung einer kanonischen Anfrage
  2. Generieren Sie eine Signaturzeichenfolge (StringToSign).
  3. Generieren eines Signaturschlüssels
  4. Generierung der Autorisierungsheaderzeichenfolge

Als Referenz ist Host, x-amz-Datum für die in canonicalRequest enthaltenen Header-Informationen erforderlich, aber es scheint, dass andere Header-Informationen hinzugefügt werden können.

import org.apache.http.util.EntityUtils;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;

private void AwsSigner4(HttpRequest request) throws Exception {

    /* X-Amz-Generierung des Datums-Headers*/
    SimpleDateFormat xAmzDateFormatter	= new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
    xAmzDateFormatter.setTimeZone(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); //Das Ablaufdatum des Unterzeichners wird von UTC festgelegt
    String xAmzDate 		= xAmzDateFormatter.format(new Date()).trim();
    request.setHeader("X-Amz-Date", xAmzDate);

    /*Generierung von Autorisierungsheaderzeichenfolgen*/
    /* 1.Regelmäßige Anforderungszeichenfolge(canonicalRequest)Generation*/
    String path 			= getPath(request);
    String xAmzContentSha   = getBodyHash(request);
    String canonicalRequest = "POST\n"
                        + path + "\n"
                        + "\n"
                        + "host:" + request.getFirstHeader("Host").getValue() + "\n"
                        + "x-amz-date:" + xAmzDate + "\n"
                        + "\n"
                        + "host;x-amz-date\n"
                        + xAmzContentSha;

    /* 2.Zeichenfolge(StringToSign)Generation*/
    String awsRegion = "ap-northeast-1" ;  //Regionsinformationen des AWS API-Anforderungsziels
    String awsNameSpace = "es" ;  //Namespace des anfordernden AWS-Service
    String StringToSign = "AWS4-HMAC-SHA256\n"
                        + xAmzDate + "\n"
                        + xAmzDate.substring(0, 8) + "/" + awsRegion 
                        + "/" + awsNameSpace +"/aws4_request\n"
                        + DigestUtils.sha256Hex(canonicalRequest);

    /* 3.Signierschlüssel(SigningKey)Generation*/
    String awsSecretAccessKey = "Geheimer AWS-Zugriffsschlüssel" ;
    // X-Amz-Datum → Region → AWS Service-Namespace → Festes Zeichen(aws4_request)Hash in der Reihenfolge von
    String hashStr    = getHmacSha256ByStrKey("AWS4" + awsSecretAccessKey, xAmzDate.substring(0, 8));
    hashStr           = getHmacSha256ByHexKey(hashStr, awsRegion);
    hashStr           = getHmacSha256ByHexKey(hashStr, awsNameSpace);
    String SigningKey = getHmacSha256ByHexKey(hashStr, "aws4_request");

    /* 4.Generierung von Autorisierungsheaderzeichenfolgen*/
    String awsAccessKeyId = "AWS-Zugriffsschlüssel-ID" ;
    String sig 			= getHmacSha256ByHexKey(SigningKey, StringToSign);
    String authorization = "AWS4-HMAC-SHA256 Credential="
                        + awsAccessKeyId
                        + "/" + xAmzDate.substring(0, 8)
                        + "/" + awsRegion
                        + "/" + awsNameSpace
                        + "/aws4_request,"
                        + "SignedHeaders=host;x-amz-date,"
                        + "Signature=" + sig;

    request.setHeader("Authorization", authorization);
}

/***Anforderungspfad abrufen*/
private String getPath(HttpRequest req) throws Exception {
    String uri				= req.getRequestLine().getUri();

    //"Trennzeichen mit URL-Abfragezeichenfolge"?Nicht in den Pfad aufnehmen
    if (uri.endsWith("?"))  uri = uri.substring(0, uri.length()-1);
    
    //Es scheint, dass es verschiedene Arten der URL-Codierung gibt, und sie codiert mit der von Amazon angegebenen Logik.
    return awsUriEncode(uri,true);
}

/***Holen Sie sich Hash von Lix und Körper*/
private String getBodyHash(HttpRequest req) throws Exception{
    HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
    String body				= EntityUtils.toString(ereq.getEntity());
    return DigestUtils.sha256Hex(body);
}

/***
    *AWS spezifizierter spezifizierter URL-Encoder
    * @param input
    * @param encodeSlash
    * @return
    * @throws UnsupportedEncodingException
    */
private String awsUriEncode(CharSequence input, boolean encodeSlash) throws UnsupportedEncodingException {
    StringBuilder result = new StringBuilder();
    boolean queryIn = false;
    for (int i = 0; i < input.length(); i++) {
        char ch = input.charAt(i);
        if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') {
            result.append(ch);
        } else if (ch == '/') {
            if (queryIn) result.append(encodeSlash ? "%2F" : ch);
            else result.append(ch);
        } else {
            if(!queryIn && ch=='?') {
                queryIn = true;
                result.append(ch);
            }else {
                byte[] bytes = new String(new char[] {ch}).getBytes("UTF-8");
                result.append("%" + Hex.encodeHexString(bytes,false));
            }
        }
    }
    return result.toString();
}

private String getHmacSha256ByStrKey(String strkey, String target) throws Exception {
    return getHmacSha256(strkey.getBytes(), target);
}

private String getHmacSha256ByHexKey(String hexkey, String target) throws Exception {
    return getHmacSha256(Hex.decodeHex(hexkey), target);
}

/***
    *HMAC verwendet Key als Zielzeichenfolge-SHA-Hash auf 256
    * @param target Hash-Ziel
    * @param key key
    * @Rückgabewert im Hex-Format
    */
private String getHmacSha256(byte[] key, String target) throws Exception {
    final Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(key, "HmacSHA256"));
    return String.valueOf(Hex.encodeHex(mac.doFinal(target.getBytes()), true));
}

Zusammenfassung

Die endgültige Zusammenfassung lautet wie folgt. Für jede Anforderung wird ein Formatierer generiert, und Konstanten werden eingebettet. Bitte korrigieren Sie ihn.

import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.binary.Hex;
import java.text.SimpleDateFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class AmazonizeInterceptor implements HttpRequestInterceptor {

	@Override
	public void process(HttpRequest request, HttpContext context) throws Exception{
        AwsSigner4(request);  
	}

    private void AwsSigner4(HttpRequest request) throws Exception {
        /* X-Amz-Generierung des Datums-Headers*/
        SimpleDateFormat xAmzDateFormatter	= new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        xAmzDateFormatter.setTimeZone(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); //Das Ablaufdatum des Unterzeichners wird von UTC festgelegt
        String xAmzDate 		= xAmzDateFormatter.format(new Date()).trim();
        request.setHeader("X-Amz-Date", xAmzDate);

        /*Generierung von Autorisierungsheaderzeichenfolgen*/
        /* 1.Regelmäßige Anforderungszeichenfolge(canonicalRequest)Generation*/
        String path 			= getPath(request);
        String xAmzContentSha   = getBodyHash(request);
        String canonicalRequest = "POST\n"
                            + path + "\n"
                            + "\n"
                            + "host:" + request.getFirstHeader("Host").getValue() + "\n"
                            + "x-amz-date:" + xAmzDate + "\n"
                            + "\n"
                            + "host;x-amz-date\n"
                            + xAmzContentSha;

        /* 2.Zeichenfolge(StringToSign)Generation*/
        String awsRegion = "ap-northeast-1" ;  //Regionsinformationen des AWS API-Anforderungsziels
        String awsNameSpace = "es" ;  //Namespace des anfordernden AWS-Service
        String StringToSign = "AWS4-HMAC-SHA256\n"
                            + xAmzDate + "\n"
                            + xAmzDate.substring(0, 8) + "/" + awsRegion 
                            + "/" + awsNameSpace +"/aws4_request\n"
                            + DigestUtils.sha256Hex(canonicalRequest);

        /* 3.Signierschlüssel(SigningKey)Generation*/
        String awsSecretAccessKey = "Geheimer AWS-Zugriffsschlüssel" ;
        // X-Amz-Datum → Region → AWS Service-Namespace → Festes Zeichen(aws4_request)Hash in der Reihenfolge von
        String hashStr    = getHmacSha256ByStrKey("AWS4" + awsSecretAccessKey, xAmzDate.substring(0, 8));
        hashStr           = getHmacSha256ByHexKey(hashStr, awsRegion);
        hashStr           = getHmacSha256ByHexKey(hashStr, awsNameSpace);
        String SigningKey = getHmacSha256ByHexKey(hashStr, "aws4_request");

        /* 4.Generierung von Autorisierungsheaderzeichenfolgen*/
        String awsAccessKeyId = "AWS-Zugriffsschlüssel-ID" ;
        String sig 			= getHmacSha256ByHexKey(SigningKey, StringToSign);
        String authorization = "AWS4-HMAC-SHA256 Credential="
                            + awsAccessKeyId
                            + "/" + xAmzDate.substring(0, 8)
                            + "/" + awsRegion
                            + "/" + awsNameSpace
                            + "/aws4_request,"
                            + "SignedHeaders=host;x-amz-date,"
                            + "Signature=" + sig;

        request.setHeader("Authorization", authorization);
    }

    /***Anforderungspfad abrufen*/
    private String getPath(HttpRequest req) throws Exception {
        String uri				= req.getRequestLine().getUri();

        //"Trennzeichen mit URL-Abfragezeichenfolge"?Nicht in den Pfad aufnehmen
        if (uri.endsWith("?"))  uri = uri.substring(0, uri.length()-1);
        
        //Es scheint, dass es verschiedene Arten der URL-Codierung gibt, und sie codiert mit der von Amazon angegebenen Logik.
        return awsUriEncode(uri,true);
    }

    /***Holen Sie sich Hash von Lix und Körper*/
    private String getBodyHash(HttpRequest req) throws Exception{
        HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
        String body				= EntityUtils.toString(ereq.getEntity());
        return DigestUtils.sha256Hex(body);
    }

    /***
        *AWS spezifizierter spezifizierter URL-Encoder
        * @param input
        * @param encodeSlash
        * @return
        * @throws UnsupportedEncodingException
        */
    private String awsUriEncode(CharSequence input, boolean encodeSlash) throws UnsupportedEncodingException {
        StringBuilder result = new StringBuilder();
        boolean queryIn = false;
        for (int i = 0; i < input.length(); i++) {
            char ch = input.charAt(i);
            if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') {
                result.append(ch);
            } else if (ch == '/') {
                if (queryIn) result.append(encodeSlash ? "%2F" : ch);
                else result.append(ch);
            } else {
                if(!queryIn && ch=='?') {
                    queryIn = true;
                    result.append(ch);
                }else {
                    byte[] bytes = new String(new char[] {ch}).getBytes("UTF-8");
                    result.append("%" + Hex.encodeHexString(bytes,false));
                }
            }
        }
        return result.toString();
    }

    private String getHmacSha256ByStrKey(String strkey, String target) throws Exception {
        return getHmacSha256(strkey.getBytes(), target);
    }

    private String getHmacSha256ByHexKey(String hexkey, String target) throws Exception {
        return getHmacSha256(Hex.decodeHex(hexkey), target);
    }

    /***
        *HMAC verwendet Key als Zielzeichenfolge-SHA-Hash auf 256
        * @param target Hash-Ziel
        * @param key key
        * @Rückgabewert im Hex-Format
        */
    private String getHmacSha256(byte[] key, String target) throws Exception {
        final Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return String.valueOf(Hex.encodeHex(mac.doFinal(target.getBytes()), true));
    }
}

Nachwort

Dieses Mal habe ich nur den Betrieb der Elasticsearch-API bestätigt. Wenn die IAM-Berechtigungen jedoch korrekt festgelegt sind, können andere APIs auf die gleiche Weise verwendet werden. Wenn es einen AWS-Service gibt, dessen Funktion bestätigt wurde, würde ich es begrüßen, wenn Sie ihn in die Kommentare schreiben könnten.

Ich hoffe, Sie finden es nützlich.

Recommended Posts

Generieren Sie AWS Signature V4 in Java und fordern Sie die API an
Generieren Sie die CloudStack-API-URL in Java
Generieren Sie OffsetDateTime aus Clock und LocalDateTime in Java
Zabbix API in Java
Versuchte Mastodons Toot- und Streaming-API in Java
Rufen Sie die Amazon Product Advertising API 5.0 (PA-API v5) in Java auf
Protokollaggregation und -analyse (Arbeiten mit AWS Athena in Java)
Java Stream API in 5 Minuten
Was ist in "Java 8 bis Java 11" passiert und wie wird eine Umgebung erstellt?
Erstellen Sie einen SlackBot mit AWS Lambda & API Gateway in Java
Aufrufen und Verwenden der API in Java (Spring Boot)
Tipps zur Verwendung von Salesforce SOAP und Bulk API in Java
Ruft Attribute und Werte aus einer XML-Datei in Java ab
Generieren Sie Stream aus einem Array primitiver Typen in Java
Implementieren Sie die XML-Signatur in Java
Sortieren Sie die Liste in absteigender Reihenfolge in Java und generieren Sie zerstörungsfrei eine neue Liste
Hinweis Nr. 1 "Zählen und Anzeigen doppelter Werte in einem Array" [Java]
Ich habe eine E-Mail in Java gesendet
Beispiel für Codierung und Decodierung in Java
Implementieren Sie reCAPTCHA v3 in Java / Spring
Versuchen Sie einen If-Ausdruck in Java
StringBuffer- und StringBuilder-Klasse in Java
Löschen von AWS S3-Objekten in Java
Analysieren der COTOHA-API-Syntaxanalyse in Java
Umbenannte Ordner in AWS S3 (Java)
Verstehe gleich und hashCode in Java
Ich habe eine Anmerkung in Java gemacht.
Versuchen Sie, AWS X-Ray in Java auszuführen
JPA (Java Persistence API) in Eclipse
Hallo Welt in Java und Gradle
Führen Sie einen externen Prozess in Java aus
Ich habe versucht, die Elasticsearch-API in Java zu verwenden
Unterschied zwischen final und Immutable in Java
Versuchen Sie es mit der Stream-API in Java
Rufen Sie die Windows-Benachrichtigungs-API in Java auf
Map ohne Verwendung eines Arrays in Java
Heutzutage Java Lambda Expressions und Stream API
Versuchen Sie es mit der JSON-Format-API in Java
Programmieren Sie PDF-Kopf- und Fußzeilen in Java
Von Java nach C und von C nach Java in Android Studio
Lesen und Schreiben von GZIP-Dateien in Java
Unterschied zwischen int und Integer in Java
Diskriminierung von Enum in Java 7 und höher
Hinweise zur Implementierung der Google Books-API in den Versionen Java Okhttp und Gson sowie Okhttp und Jackson
Zusammenfassung beim Versuch, Solr in Java zu verwenden und eine Fehlermeldung zu erhalten (Solr 6.x)
Bei Verwendung einer Liste in Java wird java.awt.List ausgegeben und ein Fehler tritt auf