Question

L'API Amazon Product Advertising (anciennement Amazon Associates Web Service ou Amazon AWS) a implémenté une nouvelle règle selon laquelle toutes les demandes de service Web qui leur sont destinées doivent être signées avant le 15 août 2009. Ils ont fourni un exemple de code sur leur site indiquant comment procéder en C # à l'aide de REST et de SOAP. L’implémentation que j’utilise est SOAP. Vous pouvez trouver l'exemple de code ici , je ne suis pas y compris parce qu'il y a une bonne quantité.

Le problème que je rencontre est que leur exemple de code utilise WSE 3 et que notre code actuel n’utilise pas WSE. Quelqu'un sait-il comment implémenter cette mise à jour en utilisant simplement le code généré automatiquement à partir du WSDL? J'aimerais ne pas avoir à passer tout de suite à la version WSE 3 si je n'ai pas à le faire car cette mise à jour est plutôt un correctif rapide pour nous retenir jusqu'à ce que nous puissions pleinement l'intégrer dans la version actuelle de dev (août 3ème, ils commencent à abandonner 1 demande sur 5, dans l'environnement en direct, s'ils ne sont pas signés, ce qui est une mauvaise nouvelle pour notre application).

Voici un extrait de la partie principale qui effectue la signature de la demande SOAP.

class ClientOutputFilter : SoapFilter
{
    // to store the AWS Access Key ID and corresponding Secret Key.
    String akid;
    String secret;

    // Constructor
    public ClientOutputFilter(String awsAccessKeyId, String awsSecretKey)
    {
        this.akid = awsAccessKeyId;
        this.secret = awsSecretKey;
    }

    // Here's the core logic:
    // 1. Concatenate operation name and timestamp to get StringToSign.
    // 2. Compute HMAC on StringToSign with Secret Key to get Signature.
    // 3. Add AWSAccessKeyId, Timestamp and Signature elements to the header.
    public override SoapFilterResult ProcessMessage(SoapEnvelope envelope)
    {
        var body = envelope.Body;
        var firstNode = body.ChildNodes.Item(0);
        String operation = firstNode.Name;

        DateTime currentTime = DateTime.UtcNow;
        String timestamp = currentTime.ToString("yyyy-MM-ddTHH:mm:ssZ");

        String toSign = operation + timestamp;
        byte[] toSignBytes = Encoding.UTF8.GetBytes(toSign);
        byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
        HMAC signer = new HMACSHA256(secretBytes);  // important! has to be HMAC-SHA-256, SHA-1 will not work.

        byte[] sigBytes = signer.ComputeHash(toSignBytes);
        String signature = Convert.ToBase64String(sigBytes); // important! has to be Base64 encoded

        var header = envelope.Header;
        XmlDocument doc = header.OwnerDocument;

        // create the elements - Namespace and Prefix are critical!
        XmlElement akidElement = doc.CreateElement(
            AmazonHmacAssertion.AWS_PFX, 
            "AWSAccessKeyId", 
            AmazonHmacAssertion.AWS_NS);
        akidElement.AppendChild(doc.CreateTextNode(akid));

        XmlElement tsElement = doc.CreateElement(
            AmazonHmacAssertion.AWS_PFX,
            "Timestamp",
            AmazonHmacAssertion.AWS_NS);
        tsElement.AppendChild(doc.CreateTextNode(timestamp));

        XmlElement sigElement = doc.CreateElement(
            AmazonHmacAssertion.AWS_PFX,
            "Signature",
            AmazonHmacAssertion.AWS_NS);
        sigElement.AppendChild(doc.CreateTextNode(signature));

        header.AppendChild(akidElement);
        header.AppendChild(tsElement);
        header.AppendChild(sigElement);

        // we're done
        return SoapFilterResult.Continue;
    }
}

Et cela s'appelle comme ceci lors de l'appel du service Web réel

// create an instance of the serivce
var api = new AWSECommerceService();

// apply the security policy, which will add the require security elements to the
// outgoing SOAP header
var amazonHmacAssertion = new AmazonHmacAssertion(MY_AWS_ID, MY_AWS_SECRET);
api.SetPolicy(amazonHmacAssertion.Policy());
Était-ce utile?

La solution

J'ai fini par mettre à jour le code pour utiliser WCF, car c'est dans la version actuelle de développement sur laquelle j'ai travaillé. Ensuite, j'ai utilisé du code qui a été publié sur les forums Amazon, mais je l'ai rendu un peu plus facile à utiliser.

UPDATE: un nouveau code plus facile à utiliser qui vous permet d'utiliser les paramètres de configuration pour tout

Dans le code précédent que j'ai posté et ce que j'ai vu ailleurs, lors de la création de l'objet de service, l'un des remplacements de constructeur est utilisé pour lui indiquer d'utiliser HTTPS, de lui attribuer l'URL HTTPS et de joindre manuellement l'inspecteur de messages. cela fera la signature. Si vous n'utilisez pas le constructeur par défaut, vous perdez la possibilité de configurer le service via le fichier de configuration.

J'ai depuis refait ce code pour que vous puissiez continuer à utiliser le constructeur par défaut sans paramètre et à configurer le service via le fichier config. L'avantage de ceci est que vous n'avez pas à recompiler votre code pour l'utiliser, ni à apporter des modifications une fois déployées telles que maxStringContentLength (c'est ce qui a provoqué cette modification et à découvrir les inconvénients de tout faire en code). . J'ai également mis à jour un peu la partie signature afin que vous puissiez lui dire quel algorithme de hachage utiliser ainsi que l'expression régulière pour extraire l'Action.

Ces deux modifications sont imputables au fait que tous les services Web d'Amazon n'utilisent pas le même algorithme de hachage et que l'action doit peut-être être extraite différemment. Cela signifie que vous pouvez réutiliser le même code pour chaque type de service en modifiant simplement le contenu du fichier de configuration.

public class SigningExtension : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(SigningBehavior); }
    }

    [ConfigurationProperty("actionPattern", IsRequired = true)]
    public string ActionPattern
    {
        get { return this["actionPattern"] as string; }
        set { this["actionPattern"] = value; }
    }

    [ConfigurationProperty("algorithm", IsRequired = true)]
    public string Algorithm
    {
        get { return this["algorithm"] as string; }
        set { this["algorithm"] = value; }
    }

    [ConfigurationProperty("algorithmKey", IsRequired = true)]
    public string AlgorithmKey
    {
        get { return this["algorithmKey"] as string; }
        set { this["algorithmKey"] = value; }
    }

    protected override object CreateBehavior()
    {
        var hmac = HMAC.Create(Algorithm);
        if (hmac == null)
        {
            throw new ArgumentException(string.Format("Algorithm of type ({0}) is not supported.", Algorithm));
        }

        if (string.IsNullOrEmpty(AlgorithmKey))
        {
            throw new ArgumentException("AlgorithmKey cannot be null or empty.");
        }

        hmac.Key = Encoding.UTF8.GetBytes(AlgorithmKey);

        return new SigningBehavior(hmac, ActionPattern);
    }
}

public class SigningBehavior : IEndpointBehavior
{
    private HMAC algorithm;

    private string actionPattern;

    public SigningBehavior(HMAC algorithm, string actionPattern)
    {
        this.algorithm = algorithm;
        this.actionPattern = actionPattern;
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(new SigningMessageInspector(algorithm, actionPattern));
    }
}

public class SigningMessageInspector : IClientMessageInspector
{
    private readonly HMAC Signer;

    private readonly Regex ActionRegex;

    public SigningMessageInspector(HMAC algorithm, string actionPattern)
    {
        Signer = algorithm;
        ActionRegex = new Regex(actionPattern);
    }

    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
    }

    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        var operation = GetOperation(request.Headers.Action);
        var timeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
        var toSignBytes = Encoding.UTF8.GetBytes(operation + timeStamp);
        var sigBytes = Signer.ComputeHash(toSignBytes);
        var signature = Convert.ToBase64String(sigBytes);

        request.Headers.Add(MessageHeader.CreateHeader("AWSAccessKeyId", Helpers.NameSpace, Helpers.AWSAccessKeyId));
        request.Headers.Add(MessageHeader.CreateHeader("Timestamp", Helpers.NameSpace, timeStamp));
        request.Headers.Add(MessageHeader.CreateHeader("Signature", Helpers.NameSpace, signature));

        return null;
    }

    private string GetOperation(string request)
    {
        var match = ActionRegex.Match(request);
        var val = match.Groups["action"];
        return val.Value;
    }
}

Pour utiliser cela, vous n'avez pas besoin de modifier votre code existant, vous pouvez même placer le code de signature dans un autre assemblage si nécessaire. Il vous suffit de configurer la section config de la manière suivante (remarque: le numéro de version est important, sans quoi le code correspondant ne sera pas chargé ou exécuté)

<system.serviceModel>
  <extensions>
    <behaviorExtensions>
      <add name="signer" type="WebServices.Amazon.SigningExtension, AmazonExtensions, Version=1.3.11.7, Culture=neutral, PublicKeyToken=null" />
    </behaviorExtensions>
  </extensions>
  <behaviors>
    <endpointBehaviors>
      <behavior name="AWSECommerceBehaviors">
        <signer algorithm="HMACSHA256" algorithmKey="..." actionPattern="\w:\/\/.+/(?&lt;action&gt;.+)" />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <basicHttpBinding>
      <binding name="AWSECommerceServiceBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true" maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536">
        <readerQuotas maxDepth="32" maxStringContentLength="16384" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
        <security mode="Transport">
          <transport clientCredentialType="None" proxyCredentialType="None" realm="" />
          <message clientCredentialType="UserName" algorithmSuite="Default" />
        </security>
      </binding>
    </basicHttpBinding>
  </bindings>
  <client>
    <endpoint address="https://ecs.amazonaws.com/onca/soap?Service=AWSECommerceService" behaviorConfiguration="AWSECommerceBehaviors" binding="basicHttpBinding" bindingConfiguration="AWSECommerceServiceBinding" contract="WebServices.Amazon.AWSECommerceServicePortType" name="AWSECommerceServicePort" />
  </client>
</system.serviceModel>

Autres conseils

Hey Brian, je traite le même problème dans mon application. J'utilise le code généré par WSDL. En fait, je l'ai encore généré aujourd'hui pour garantir la dernière version. J'ai trouvé que signer avec un certificat X509 était le chemin le plus simple. Après quelques minutes d’essais à mon actif, jusqu’à présent, cela semble bien fonctionner. En gros, vous changez de:

AWSECommerceService service = new AWSECommerceService();
// ...then invoke some AWS call

À:

AWSECommerceService service = new AWSECommerceService();
service.ClientCertificates.Add(X509Certificate.CreateFromCertFile(@"path/to/cert.pem"));
// ...then invoke some AWS call

Viper sur bytesblocks.com a publié plus de détails , y compris comment obtenir le certificat X509 généré par Amazon pour vous.

MODIFIER : en tant que discussion ici indique que cela pourrait ne pas signer la demande. Publierai au fur et à mesure que j'apprendrai.

MODIFIER : cela ne semble pas du tout signer la demande. Au lieu de cela, il semble nécessiter une connexion https et utilise le certificat pour l'authentification du client SSL. L'authentification client SSL est une fonctionnalité peu utilisée de SSL. Cela aurait été bien si l'API de publicité du produit Amazon l'avait prise en charge en tant que mécanisme d'authentification! Malheureusement, cela ne semble pas être le cas. La preuve est double: (1) ce n'est pas l'un des documenté schémas d'authentification et (2) le certificat que vous spécifiez importe peu.

Une certaine confusion est ajoutée par Amazon qui n’impose toujours pas l’authentification des demandes même après leur proclamation de la date limite du 15 août 2009. Cela donne l'impression que les demandes se transmettent correctement lors de l'ajout du certificat, même s'il risque de ne pas ajouter de valeur.

Regardez la réponse de Brian Surowiec pour une solution qui fonctionne. Je laisse cette réponse ici pour documenter cette approche attrayante, mais apparemment ratée, car je peux encore en voir la discussion dans les blogs et les forums Amazon.

Vous pouvez le faire en utilisant les attributs ProtectionLevel . Voir Comprendre le niveau de protection .

L’implémentation de la signature dans le savon est un peu vilaine. Je l’ai fait en PHP pour l’utiliser sur http://www.apisigning.com/ . L'astuce que j'ai finalement trouvée est que les paramètres Signature, AWSAccessKey et Timestamp doivent figurer dans l'en-tête SOAP. De plus, la signature n'est qu'un hachage de l'opération + l'horodatage et ne nécessite aucun paramètre.

Je ne sais pas comment cela s'intègre dans C #, mais je pensais qu'il pourrait être utile

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top