Как подписать запрос веб-службы Amazon в .NET с SOAP и без WSE - PullRequest
3 голосов
/ 30 июля 2009

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

Проблема, с которой я сталкиваюсь, состоит в том, что их пример кода использует WSE 3, а наш текущий код не использует WSE. Кто-нибудь знает, как реализовать это обновление, просто используя автоматически сгенерированный код из WSDL? Я бы не хотел переключаться на WSE 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());

Ответы [ 4 ]

7 голосов
/ 01 августа 2009

Я закончил тем, что обновил код для использования 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>
1 голос
/ 30 июля 2009

Привет, Брайан, я имею дело с той же проблемой в моем приложении. Я использую код, сгенерированный 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

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

РЕДАКТИРОВАТЬ : как показывает обсуждение здесь , это может фактически не подписывать запрос. Выложу как узнаю больше.

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

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

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

0 голосов
/ 28 августа 2009

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

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

0 голосов
/ 30 июля 2009

Вы можете сделать это, используя атрибуты ProtectionLevel. См. Уровень защиты .

...