Как подписать запрос веб-службы Amazon в .NET с помощью SOAP и без WSE

StackOverflow https://stackoverflow.com/questions/1204191

Вопрос

Amazon Product Advertising API (ранее Amazon Associates Web Service или Amazon AWS) внедрил новое правило, согласно которому к 15 августа 2009 года все запросы веб-сервисов к ним должны быть подписаны.Они предоставили пример кода на своем сайте, показывающий, как это сделать на C #, используя как REST, так и SOAP.Реализация, которую я использую, - это SOAP.Вы можете найти пример кода здесь, Я не включаю это, потому что их довольно много.

Проблема, с которой я столкнулся, заключается в том, что их пример кода использует WSE 3, а наш текущий код не использует WSE.Кто-нибудь знает, как реализовать это обновление, просто используя автоматически сгенерированный код из WSDL?Я бы не хотел переключаться на WSE 3 прямо сейчас, если в этом нет необходимости, поскольку это обновление - скорее быстрый патч, который задержит нас до тех пор, пока мы не сможем полностью реализовать это в текущей версии разработчика (3 августа они начинают отбрасывать 1 из 5 запросов в живой среде, если они не подписаны, что является плохой новостью для нашего приложения).

Вот фрагмент основной части, которая выполняет фактическую подпись 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;
    }
}

И это вызывается следующим образом при выполнении фактического вызова веб-службы

// 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());
Это было полезно?

Решение

Я закончил тем, что обновлял код для использования WCF, так как это то, что есть в текущей версии разработчика, над которой я работал. Затем я использовал некоторый код, который был размещен на форумах Amazon, но немного облегчил его использование.

ОБНОВЛЕНИЕ: новый, более простой в использовании код, который позволяет вам по-прежнему использовать параметры конфигурации для всего

В предыдущем коде, который я опубликовал, и то, что я видел в другом месте, когда создается сервисный объект, используется одно из переопределений конструктора, чтобы указать ему использовать HTTPS, присвоить ему URL-адрес HTTPS и вручную присоединить инспектор сообщений. это будет делать подписание. Недостатком неиспользования конструктора по умолчанию является то, что вы теряете возможность конфигурировать сервис через файл конфигурации.

С тех пор я переделал этот код, чтобы вы могли продолжить использовать конструктор по умолчанию, без параметров и настроить сервис через файл конфигурации. Преимущество этого заключается в том, что вам не нужно перекомпилировать свой код, чтобы использовать его, или вносить изменения после развертывания, например, в maxStringContentLength (что и послужило причиной этого изменения, а также обнаружить недостатки при выполнении всего этого в коде) , Я также немного обновил часть подписи, чтобы вы могли указать, какой алгоритм хеширования использовать, а также регулярное выражение для извлечения Action.

Эти два изменения связаны с тем, что не все веб-сервисы Amazon используют один и тот же алгоритм хеширования, и действие может потребоваться извлечь по-разному. Это означает, что вы можете повторно использовать один и тот же код для каждого типа службы, просто изменив то, что находится в файле конфигурации.

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

Чтобы использовать это, вам не нужно вносить какие-либо изменения в существующий код, вы даже можете поместить код подписи в другую сборку, если это будет необходимо. Вам просто нужно настроить раздел конфигурации следующим образом (примечание: номер версии важен, без совпадения код не будет загружаться или запускаться)

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

Другие советы

Эй, Брайан, я сталкиваюсь с той же проблемой в своем приложении.Я использую сгенерированный WSDL код - фактически, я сгенерировал его снова сегодня, чтобы убедиться в актуальности версии.Я обнаружил, что подписание с помощью сертификата X509 - самый простой путь.После нескольких минут тестирования у меня за плечами, пока, похоже, все работает нормально.По сути, вы меняетесь с:

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

Гадюка в bytesblocks.com опубликовано более подробная информация, в том числе о том, как получить сертификат X509, который Amazon генерирует для вас.

Редактировать:по мере обсуждения здесь указывает, что это может на самом деле не подписывать запрос.Опубликую по мере того, как узнаю больше.

Редактировать:похоже, что это вообще не подписывает запрос.Вместо этого, похоже, требуется https-соединение, и он использует сертификат для аутентификации клиента SSL.Аутентификация клиента SSL - это нечасто используемая функция SSL.Было бы неплохо, если бы рекламный API продукта Amazon поддерживал его в качестве механизма аутентификации!К сожалению, похоже, что это не так.Доказательства двоякие:(1) это не один из документированные схемы аутентификации, и (2) не имеет значения, какой сертификат вы укажете.

Некоторую путаницу вносит то, что Amazon по-прежнему не применяет аутентификацию к запросам даже после того, как был объявлен крайний срок 15 августа 2009 года.Это делает так, что запросы передаются корректно при добавлении сертификата, даже если это может не добавить никакого значения.

Посмотрите на ответ Брайана Суровца, чтобы найти решение, которое работает.Я оставляю этот ответ здесь, чтобы задокументировать привлекательный, но явно неудачный подход, поскольку я все еще вижу, как он обсуждается в блогах и на форумах Amazon.

Это можно сделать с помощью атрибутов ProtectionLevel . См. Общие сведения об уровне защиты .

Мыльная реализация подписи довольно противная. Я сделал это на PHP для использования на http://www.apisigning.com/ . Уловка, которую я наконец понял, заключалась в том, что параметры Signature, AWSAccessKey и Timestamp должны быть указаны в заголовке SOAP. Кроме того, подпись - это просто хэш метки времени Operation + и не требует включения каких-либо параметров.

Я не уверен, как это вписывается в C #, но подумал, что это может быть полезно

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top