Générer AWS Signature V4 en Java et demander l'API

introduction

L'API AWS peut tirer parti de l'authentification IAM en signant la demande avec AWS Signature V4. Normalement, vous n'avez pas besoin d'être très conscient des spécifications de Signature V4 en utilisant le kit AWS SDK, mais pour diverses raisons, j'ai eu l'occasion d'implémenter Signature V4 par moi-même, je vais donc laisser une note.

Implémenté dans Java 11, le client HTTP utilise le client Apache HttpComponents (https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient).

Le code est copié-collé et traité, il est donc un peu douteux qu'il fonctionnera correctement.

Demander lorsque vous ne signez pas

Par souci de clarté, voici un exemple de demande lorsque vous ne signez pas. Ceci est un exemple de demande de recherche utilisant l'API Elasticsearch. Étant donné que cet exemple utilise la méthode POST, les paramètres de requête sont écrits dans le corps HTTP.

Nous signerons cette demande avec AWS Signature V4 et la modifierons afin qu'elle ne reçoive que les API des utilisateurs authentifiés.

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(){
    //Demande de génération
    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( "Chaîne de requête(réduction)", "UTF-8"));

    //Exécuter une requête avec un client HTTP
    HttpClient client       = HttpClientBuilder.create().build();
    CloseableHttpResponse res = (CloseableHttpResponse) client.execute(post);

    // ...Traitement pour réponse...
}

Informations sur les signatures

AWS Signature V4 est disponible sur le site Web d'Amazon. J'ai obtenu les informations en suivant l'URL ci-dessous. https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

En outre, des informations sur la façon d'obtenir l'ID de clé d'accès et la clé d'accès secrète requises pour l'authentification sont obtenues à partir de l'URL suivante. https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-sec-cred-types.html

Implémentation de signe

AWS Signature V4 génère un hachage à l'aide des informations de demande elles-mêmes et de l'ID de clé d'accès / clé d'accès secrète émise à l'avance, et l'attache à l'en-tête de la demande. J'ai fait du hachage encore et encore, et je ne savais pas jusqu'où se trouvait la cible du hachage, alors j'ai fait un essai et une erreur.

Création de la classe HttpRequestInterceptor

Tout d'abord, afin d'obtenir les informations de la requête à envoyer, préparez la classe HttpRequestInterceptor et le client HTTP qui la prend en sandwich. En insérant cette classe lors de la génération d'un client HTTP, vous pouvez insérer un traitement juste avant d'envoyer une requête au serveur.

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 {
        /*Implémentez AWS Signature V4 dans cette implémentation de fonction.*/
    }
}

--Modification de l'appelant du client HTTP

//...Abréviation...

void reqFunc(){
    //...Abréviation...

    //Exécuter une requête avec un client HTTP
    HttpClient client = HttpClientBuilder.create()
                        .addInterceptorLast(new AmazonizeInterceptor())  //← Ajouter ceci
                        .build();
    CloseableHttpResponse res = (CloseableHttpResponse) client.execute(post);

    //...Traitement pour réponse...
}

Désormais, lorsque vous effectuez une requête avec un client HTTP prenant en sandwich RequestInterceptor, la fonction AwsSigner4 définie précédemment sera exécutée avant d'être envoyée au serveur.

Implémentation de la fonction AwsSigner4

Ensuite, nous implémenterons la fonction AwsSigner4 qui effectue le processus de signature réel. Tout ce que vous avez à faire est d'ajouter "X-Amz-Date" et "Authorization" à l'en-tête de la requête, mais il faut plusieurs étapes pour trouver la valeur de Authorization.

  1. Génération de requête canonique
  2. Générez une chaîne de signature (StringToSign)
  3. Génération d'une clé de signature
  4. Génération de la chaîne d'en-tête d'autorisation

Pour référence, host, x-amz-date est requis pour les informations d'en-tête incluses dans le canonicalRequest, mais il semble que d'autres informations d'en-tête puissent être ajoutées.

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-Génération d'en-tête de date*/
    SimpleDateFormat xAmzDateFormatter	= new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
    xAmzDateFormatter.setTimeZone(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); //La date d'expiration du signataire est déterminée par UTC
    String xAmzDate 		= xAmzDateFormatter.format(new Date()).trim();
    request.setHeader("X-Amz-Date", xAmzDate);

    /*Génération de chaîne d'en-tête d'autorisation*/
    /* 1.Chaîne de demande régulière(canonicalRequest)Génération*/
    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.Chaîne de signe(StringToSign)Génération*/
    String awsRegion = "ap-northeast-1" ;  //Informations sur la région de la destination de la demande d'API AWS
    String awsNameSpace = "es" ;  //Espace de nom du service AWS demandeur
    String StringToSign = "AWS4-HMAC-SHA256\n"
                        + xAmzDate + "\n"
                        + xAmzDate.substring(0, 8) + "/" + awsRegion 
                        + "/" + awsNameSpace +"/aws4_request\n"
                        + DigestUtils.sha256Hex(canonicalRequest);

    /* 3.Clé de signature(SigningKey)Génération*/
    String awsSecretAccessKey = "Clé d'accès secrète AWS" ;
    // X-Amz-Date → Région → Espace de noms du service AWS → Caractère fixe(aws4_request)Hash dans l'ordre de
    String hashStr    = getHmacSha256ByStrKey("AWS4" + awsSecretAccessKey, xAmzDate.substring(0, 8));
    hashStr           = getHmacSha256ByHexKey(hashStr, awsRegion);
    hashStr           = getHmacSha256ByHexKey(hashStr, awsNameSpace);
    String SigningKey = getHmacSha256ByHexKey(hashStr, "aws4_request");

    /* 4.Génération de chaîne d'en-tête d'autorisation*/
    String awsAccessKeyId = "ID de clé d'accès AWS" ;
    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);
}

/***Obtenir le chemin de la demande*/
private String getPath(HttpRequest req) throws Exception {
    String uri				= req.getRequestLine().getUri();

    //"Séparateur avec chaîne de requête URL"?Ne pas inclure dans le chemin
    if (uri.endsWith("?"))  uri = uri.substring(0, uri.length()-1);
    
    //Il semble qu'il existe différents types de codage d'URL, et il encode avec la logique spécifiée par Amazon.
    return awsUriEncode(uri,true);
}

/***Obtenez du hasch de lix et du corps*/
private String getBodyHash(HttpRequest req) throws Exception{
    HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
    String body				= EntityUtils.toString(ereq.getEntity());
    return DigestUtils.sha256Hex(body);
}

/***
    *Encodeur d'URL de spécification AWS
    * @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 utilisant la clé comme chaîne cible-SHA-Hash à 256
    * @Param target Hash target
    * @touche clé param
    * @retourne la valeur de hachage au format hexadécimal
    */
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));
}

Résumé

Le résumé final est le suivant. Un formateur est généré pour chaque requête et des constantes sont intégrées, mais veuillez le corriger.

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-Génération d'en-tête de date*/
        SimpleDateFormat xAmzDateFormatter	= new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        xAmzDateFormatter.setTimeZone(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); //La date d'expiration du signataire est déterminée par UTC
        String xAmzDate 		= xAmzDateFormatter.format(new Date()).trim();
        request.setHeader("X-Amz-Date", xAmzDate);

        /*Génération de chaîne d'en-tête d'autorisation*/
        /* 1.Chaîne de demande régulière(canonicalRequest)Génération*/
        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.Chaîne de signe(StringToSign)Génération*/
        String awsRegion = "ap-northeast-1" ;  //Informations sur la région de la destination de la demande d'API AWS
        String awsNameSpace = "es" ;  //Espace de nom du service AWS demandeur
        String StringToSign = "AWS4-HMAC-SHA256\n"
                            + xAmzDate + "\n"
                            + xAmzDate.substring(0, 8) + "/" + awsRegion 
                            + "/" + awsNameSpace +"/aws4_request\n"
                            + DigestUtils.sha256Hex(canonicalRequest);

        /* 3.Clé de signature(SigningKey)Génération*/
        String awsSecretAccessKey = "Clé d'accès secrète AWS" ;
        // X-Amz-Date → Région → Espace de noms du service AWS → Caractère fixe(aws4_request)Hash dans l'ordre de
        String hashStr    = getHmacSha256ByStrKey("AWS4" + awsSecretAccessKey, xAmzDate.substring(0, 8));
        hashStr           = getHmacSha256ByHexKey(hashStr, awsRegion);
        hashStr           = getHmacSha256ByHexKey(hashStr, awsNameSpace);
        String SigningKey = getHmacSha256ByHexKey(hashStr, "aws4_request");

        /* 4.Génération de chaîne d'en-tête d'autorisation*/
        String awsAccessKeyId = "ID de clé d'accès AWS" ;
        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);
    }

    /***Obtenir le chemin de la demande*/
    private String getPath(HttpRequest req) throws Exception {
        String uri				= req.getRequestLine().getUri();

        //"Séparateur avec chaîne de requête URL"?Ne pas inclure dans le chemin
        if (uri.endsWith("?"))  uri = uri.substring(0, uri.length()-1);
        
        //Il semble qu'il existe différents types de codage d'URL, et il encode avec la logique spécifiée par Amazon.
        return awsUriEncode(uri,true);
    }

    /***Obtenez du hasch de lix et du corps*/
    private String getBodyHash(HttpRequest req) throws Exception{
        HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
        String body				= EntityUtils.toString(ereq.getEntity());
        return DigestUtils.sha256Hex(body);
    }

    /***
        *Encodeur d'URL de spécification AWS
        * @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 utilisant la clé comme chaîne cible-SHA-Hash à 256
        * @Param target Hash target
        * @touche clé param
        * @retourne la valeur de hachage au format hexadécimal
        */
    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));
    }
}

Épilogue

Cette fois, je n'ai confirmé que le fonctionnement de l'API Elasticsearch, mais si les autorisations IAM sont définies correctement, je pense que d'autres API peuvent être utilisées de la même manière. S'il existe un service AWS dont le fonctionnement a été confirmé, je vous serais reconnaissant de bien vouloir l'écrire dans les commentaires.

J'espère que tu trouves cela utile.

Recommended Posts

Générer AWS Signature V4 en Java et demander l'API
Générer l'URL de l'API CloudStack en Java
Générer OffsetDateTime à partir de Clock et LocalDateTime en Java
API Zabbix en Java
Essayé l'API Toot et Streaming de Mastodon en Java
Appelez l'API Amazon Product Advertising 5.0 (PA-API v5) en Java
Agrégation et analyse de journaux (utilisation d'AWS Athena en Java)
API Java Stream en 5 minutes
Que s'est-il passé dans «Java 8 to Java 11» et comment créer un environnement
Créer un SlackBot avec AWS lambda et API Gateway en Java
Comment appeler et utiliser l'API en Java (Spring Boot)
Conseils d'utilisation de Salesforce SOAP et de l'API Bulk en Java
Obtenir des attributs et des valeurs à partir d'un fichier XML en Java
Générer un flux à partir d'un tableau de types primitifs en Java
Implémenter la signature XML en Java
Trier la liste par ordre décroissant en Java et générer une nouvelle liste de manière non destructive
Note n ° 1 "Comptage et affichage des valeurs en double dans un tableau" [Java]
J'ai envoyé un e-mail en Java
Exemple d'encodage et de décodage en Java
Implémenter reCAPTCHA v3 dans Java / Spring
Essayez une expression If en Java
Classe StringBuffer et StringBuilder en Java
Suppression d'objets AWS S3 dans Java
Analyser l'analyse syntaxique de l'API COTOHA en Java
Dossiers renommés dans AWS S3 (Java)
Comprendre equals et hashCode en Java
J'ai fait une annotation en Java.
Essayez d'exécuter AWS X-Ray en Java
JPA (API de persistance Java) dans Eclipse
Bonjour tout le monde en Java et Gradle
Exécuter un processus externe en Java
J'ai essayé d'utiliser l'API Elasticsearch en Java
Différence entre final et immuable en Java
Essayez d'utiliser l'API Stream en Java
Appelez l'API de notification Windows en Java
Mapper sans utiliser de tableau en java
De nos jours, les expressions Java Lambda et l'API de flux
Essayez d'utiliser l'API au format JSON en Java
Programmer les en-têtes et pieds de page PDF en Java
De Java à C et de C à Java dans Android Studio
Lire et écrire des fichiers gzip en Java
Différence entre int et Integer en Java
Discrimination d'énum dans Java 7 et supérieur
Remarques sur la mise en œuvre de l'API Google Books dans les versions java okhttp et gson et les versions okhttp et jackson
Résumé en essayant d'utiliser Solr en Java et en obtenant une erreur (Solr 6.x)
Lors de l'utilisation d'une liste en Java, java.awt.List sort et une erreur se produit