Domanda

L'API di Amazon Product Advertising (precedentemente Amazon Associates Web Service o Amazon AWS) ha implementato una nuova regola che è entro il 15 agosto 2009 tutte le richieste di servizi Web devono essere firmate. Hanno fornito un codice di esempio sul loro sito che mostra come eseguire questa operazione in C # utilizzando REST e SOAP. L'implementazione che sto usando è SOAP. Puoi trovare il codice di esempio qui , non lo sono includendolo perché c'è un giusto importo.

Il problema che sto riscontrando è che il loro codice di esempio utilizza WSE 3 e il nostro codice attuale non utilizza WSE. Qualcuno sa come implementare questo aggiornamento usando semplicemente il codice generato automaticamente dal WSDL? Vorrei non dover passare al materiale WSE 3 in questo momento se non devo farlo poiché questo aggiornamento è più di una patch rapida per trattenerci fino a quando non saremo in grado di implementarlo completamente nell'attuale versione di sviluppo (agosto In terzo luogo stanno iniziando a eliminare 1 richiesta su 5, nell'ambiente live, se non sono firmati, il che è una cattiva notizia per la nostra applicazione).

Ecco uno snippet della parte principale che esegue la firma effettiva della richiesta 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;
    }
}

E questo viene chiamato in questo modo quando si effettua l'effettiva chiamata al servizio web

// 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());
È stato utile?

Soluzione

Ho finito per aggiornare il codice per usare WCF poiché è quello che è nell'attuale versione di sviluppo su cui ho lavorato. Quindi ho usato un po 'di codice che è stato pubblicato sui forum di Amazon, ma mi ha reso un po' più facile da usare.

AGGIORNAMENTO: nuovo codice più facile da usare che ti consente di utilizzare ancora le impostazioni di configurazione per tutto

Nel codice precedente che ho pubblicato, e quello che ho visto altrove, quando viene creato l'oggetto di servizio viene utilizzato uno degli override del costruttore per dirgli di usare HTTPS, dargli l'URL HTTPS e collegare manualmente l'ispettore dei messaggi che farà la firma. L'aspetto negativo di non utilizzare il costruttore predefinito è la perdita della possibilità di configurare il servizio tramite il file di configurazione.

Da allora ho rifatto questo codice in modo da poter continuare a utilizzare il costruttore predefinito, senza parametri, e configurare il servizio tramite il file di configurazione. Il vantaggio di questo è che non è necessario ricompilare il codice per utilizzarlo o apportare modifiche una volta distribuito, ad esempio maxStringContentLength (che è ciò che ha causato questo cambiamento, nonché scoprire gli svantaggi di fare tutto nel codice) . Ho anche aggiornato un po 'la parte della firma in modo da poter dire quale algoritmo di hash usare e la regex per estrarre l'Azione.

Questi due cambiamenti sono dovuti al fatto che non tutti i servizi Web di Amazon utilizzano lo stesso algoritmo di hashing e potrebbe essere necessario estrarre l'Azione in modo diverso. Ciò significa che puoi riutilizzare lo stesso codice per ogni tipo di servizio semplicemente modificando ciò che è nel file di configurazione.

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;
    }
}

Per utilizzare questo non è necessario apportare modifiche al codice esistente, è possibile anche inserire il codice di firma in un altro intero assembly, se necessario. Devi solo impostare la sezione di configurazione in questo modo (nota: il numero di versione è importante, senza che corrisponda al codice non verrà caricato o eseguito)

<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>

Altri suggerimenti

Ehi Brian, sto affrontando lo stesso problema nella mia app. Sto usando il codice generato da WSDL, infatti l'ho generato di nuovo oggi per garantire la versione più recente. Ho scoperto che firmare con un certificato X509 è il percorso più semplice. Con alcuni minuti di test sotto la cintura, finora sembra funzionare bene. In sostanza cambi da:

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

A:

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

Viper su bytesblocks.com ha pubblicato maggiori dettagli , incluso come ottenere il certificato X509 che Amazon genera per te.

EDIT : come la discussione qui indica che in realtà potrebbe non firmare la richiesta. Pubblicherò mentre imparo di più.

MODIFICA : questo non sembra firmare affatto la richiesta. Invece, sembra richiedere una connessione https e utilizza il certificato per l'autenticazione client SSL. L'autenticazione client SSL è una funzionalità poco utilizzata di SSL. Sarebbe stato bello se l'API di pubblicità del prodotto Amazon lo supportasse come meccanismo di autenticazione! Purtroppo questo non sembra essere il caso. Le prove sono duplici: (1) non è uno dei documentato schemi di autenticazione e (2) non importa quale certificato specifichi.

Un po 'di confusione è stata aggiunta da Amazon che non applica ancora l'autenticazione sulle richieste anche dopo la loro proclamazione della scadenza del 15 agosto 2009. In questo modo le richieste sembrano passare correttamente quando viene aggiunto il certificato, anche se potrebbe non aggiungere alcun valore.

Guarda la risposta di Brian Surowiec per una soluzione che funzioni. Lascio questa risposta qui per documentare l'approccio accattivante ma apparentemente fallito, come posso ancora vederlo discusso nei blog e nei forum di Amazon.

Puoi farlo usando gli attributi ProtectionLevel . Vedi Comprensione del livello di protezione .

L'implementazione soap della firma è piuttosto brutta. L'ho fatto in PHP per l'uso su http://www.apisigning.com/ . Il trucco che ho finalmente capito è che i parametri Signature, AWSAccessKey e Timestamp devono andare nell'intestazione SOAP. Inoltre, la firma è solo un hash di Operation + timestamp e non deve includere alcun parametro.

Non sono sicuro di come si adatti a C #, ma ho pensato che potesse essere di qualche utilità

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top