C# 如何在.NET中使用SOAP和不使用WSE对Amazon web服务请求进行签名

C# 如何在.NET中使用SOAP和不使用WSE对Amazon web服务请求进行签名,c#,wcf,web-services,soap,amazon-web-services,C#,Wcf,Web Services,Soap,Amazon Web Services,亚马逊产品广告API(前身为亚马逊联合网络服务或亚马逊AWS)实施了一项新规则,即在2009年8月15日之前,所有向其发出的网络服务请求都必须签名。他们在网站上提供了示例代码,展示了如何在C#中使用REST和SOAP实现这一点。我正在使用的实现是SOAP。你可以找到示例代码,我不包括它,因为有一个公平的数量 我遇到的问题是,他们的示例代码使用WSE3,而我们当前的代码不使用WSE。有人知道如何仅使用WSDL中自动生成的代码来实现此更新吗?如果不需要的话,我不想现在就切换到WSE3,因为这个更新更

亚马逊产品广告API(前身为亚马逊联合网络服务或亚马逊AWS)实施了一项新规则,即在2009年8月15日之前,所有向其发出的网络服务请求都必须签名。他们在网站上提供了示例代码,展示了如何在C#中使用REST和SOAP实现这一点。我正在使用的实现是SOAP。你可以找到示例代码,我不包括它,因为有一个公平的数量

我遇到的问题是,他们的示例代码使用WSE3,而我们当前的代码不使用WSE。有人知道如何仅使用WSDL中自动生成的代码来实现此更新吗?如果不需要的话,我不想现在就切换到WSE3,因为这个更新更像是一个快速补丁,可以让我们在当前的开发版本中完全实现它(8月3日,在实时环境中,如果未签名,他们将开始减少五分之一的请求,这对我们的应用程序来说是个坏消息)

下面是对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;
    }
}
在进行实际的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());

您可以使用
ProtectionLevel
属性来实现这一点。请参阅。

嘿,Brian,我在我的应用程序中处理相同的问题。我使用WSDL生成的代码——事实上,我今天再次生成了该代码以确保最新版本。我发现使用X509证书签名是最简单的途径。只需几分钟的测试即可完成r我的腰带,到目前为止,它似乎工作正常。基本上,你从:

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

Viper在bytesblocks.com发布,包括如何获得亚马逊为您生成的X509证书

编辑:如讨论所示,这可能不会实际签署请求。我将在了解更多信息后发布

编辑:这似乎根本不是对请求进行签名。相反,它似乎需要https连接,并使用证书进行SSL客户端身份验证。SSL客户端身份验证是SSL的一项不常使用的功能。如果亚马逊产品广告API支持它作为身份验证机制,那就太好了nism!不幸的是,情况似乎并非如此。证据有两个:(1)它不是其中一个,和(2)你指定的证书无关紧要

亚马逊甚至在宣布了2009年8月15日的截止日期之后仍然没有对请求实施身份验证,这增加了一些混乱。这使得请求在添加证书时看起来是正确通过的,尽管它可能不会增加任何价值


看看Brian Surowiec的答案,这是一个有效的解决方案。我在这里留下这个答案是为了记录这个吸引人但显然失败的方法,因为我仍然可以在博客和Amazon论坛上看到它的讨论。

我最后更新了代码以使用WCF,因为这是我一直在开发的当前开发版本中的代码。然后我使用了一些代码这是在亚马逊论坛上发布的,但使它更易于使用

更新:新的更易于使用的代码,允许您仍然对所有内容使用配置设置

在我之前发布的代码中,以及我在其他地方看到的,当创建服务对象时,会使用一个构造函数重写来告诉它使用HTTPS,为其提供HTTPS url,并手动附加将进行签名的消息检查器。不使用默认构造函数的缺点是您无法配置通过配置文件提供服务

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;
    }
}
我重新编写了这段代码,这样您就可以继续使用默认的无参数构造函数,并通过配置文件配置服务。这样做的好处是,您不必重新编译代码来使用它,也不必在部署后进行更改,例如对maxStringContentLength进行更改(这就是导致此更改发生的原因,同时也发现了在代码中执行所有操作的缺点)。我还对签名部分进行了一些更新,以便您可以告诉它使用什么哈希算法以及提取操作的正则表达式

这两个更改是因为并非所有来自Amazon的web服务都使用相同的哈希算法,并且可能需要以不同的方式提取操作。这意味着您只需更改配置文件中的内容,就可以为每种服务类型重用相同的代码

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;
    }
}
要使用它,您不需要对现有代码进行任何更改,如果需要,您甚至可以将签名代码放入整个其他程序集中。您只需将配置部分设置为这样(注意:版本号很重要,如果不匹配,代码将不会加载或运行)


签名的soap实现有点令人讨厌。我在PHP中这样做是为了在上使用。我最终发现的诀窍是签名、AWSAccessKey和时间戳参数需要放在soap头中。此外,签名只是操作+时间戳的散列,不需要包含任何参数


我不确定这是否适用于C#,但我认为这可能会有一些用处

你在亚马逊有联系人吗?他们需要了解为什么他们不应该要求人们使用几乎不受支持的过时软件(WSE).查看我的个人资料,让他们通过live.com上的johnwsaundersiii与我联系。嗨,John,是的,我们在那里有一个联系人。我会考虑将消息传递给他们。我最终做了同样的事情,可能是基于同一论坛帖子——转换为WCF并在端点行为中添加了签名。不过有两个区别:(1)我必须使用XmlDictionaryWriter.WriteString而不是XmlDictionaryWriter.WriteElementString。WriteElementString导致头元素写入两次,一次写入另一个。(2)一次写入
<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>