C# 如何使用 SOAP 和不使用 WSE 在 .NET 中签署 Amazon Web 服务请求

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1204191/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 10:50:38  来源:igfitidea点击:

How to sign an Amazon web service request in .NET with SOAP and without WSE

c#wcfweb-servicessoapamazon-web-services

提问by Brian Surowiec

The Amazon Product Advertising API (formerly Amazon Associates Web Service or Amazon AWS) has implemented a new rule which is by August 15th 2009 all web service requests to them must be signed. They have provided sample code on their site showing how to do this in C# using both REST and SOAP. The implementation that I'm using is SOAP. You can find the sample code here, I'm not including it because there is a fair amount.

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

The problem I'm having is their sample code uses WSE 3 and our current code doesn't use WSE. Does anyone know how to implement this update with just using the auto generated code from the WSDL? I'd like to not have to switch over to the WSE 3 stuff right now if I don't have to since this update is more of a quick patch to hold us over until we can fully implement this in the current dev version (August 3rd they're starting to drop 1 in 5 requests, in the live environment, if they aren't signed which is bad news for our application).

我遇到的问题是他们的示例代码使用 WSE 3 而我们当前的代码不使用 WSE。有谁知道如何仅使用 WSDL 自动生成的代码来实现此更新?如果我不需要,我不想现在就切换到 WSE 3 的东西,因为这个更新更像是一个快速补丁,可以让我们坚持到我们可以在当前的开发版本中完全实现它(八月第三,在实时环境中,如果没有签名,他们将开始丢弃 5 个请求中的 1 个,这对我们的应用程序来说是个坏消息)。

Here's a snippet of the main portion that does the actual signing of the SOAP request.

下面是对 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;
    }
}

And that gets called like this when making the actual web service call

在进行实际的 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());

采纳答案by Brian Surowiec

I ended up updating the code to use WCF since that's what it is in the current dev version I've been working on. Then I used some code that was posted on the Amazon forums, but made it a little easier to use.

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

UPDATE:new easier to use code that lets you still use the config settings for everything

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

In the previous code I posted, and what I've seen elsewhere, when the service object is created one of the constructor overrides is used to tell it to use HTTPS, give it the HTTPS url and to manually attach the message inspector that will do the signing. The downfall to not using the default constructor is you lose the ability to configure the service via the config file.

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

I've since redone this code so you can continue to use the default, parameterless, constructor and configure the service via the config file. The benifit of this is you don't have to recompile your code to use this, or make changes once deployed such as to maxStringContentLength (which is what caused this change to take place as well as discover the downfalls to doing it all in code). I also updated the signing part a bit so that way you can tell it what hashing algorithm to use as well as the regex for extracting the Action.

我已经重做了这段代码,以便您可以继续使用默认的、无参数的构造函数并通过配置文件配置服务。这样做的好处是您不必重新编译代码即可使用它,也不必在部署后进行更改,例如 maxStringContentLength(这是导致此更改发生的原因以及发现在代码中执行所有操作的缺点) . 我还稍微更新了签名部分,这样你就可以告诉它使用什么散列算法以及用于提取操作的正则表达式。

These two changes are because not all web services from Amazon use the same hashing algorithm and the Action might need to be extracted differently. This means you can reuse the same code for each service type just by changing what's in the config file.

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

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

To use this you don't need to make any changes to your existing code, you can even put the signing code in a whole other assembly if need be. You just need to set up the config section as so (note: the version number is important, without it matching the code will not load or run)

要使用它,您无需对现有代码进行任何更改,如果需要,您甚至可以将签名代码放在整个其他程序集中。你只需要这样设置配置部分(注意:版本号很重要,没有它匹配的代码将不会加载或运行)

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

回答by John Saunders

You can do this using the ProtectionLevelattributes. See Understanding Protection Level.

您可以使用ProtectionLevel属性执行此操作。请参阅了解保护级别

回答by Oren Trutner

Hey Brian, I'm dealing with the same issue in my app. I'm using the WSDL generated code -- in fact I generated it again today to ensure the latest version. I found that signing with an X509 certificate the most straightforward path. With a few minutes of testing under my belt, so far it appears to work okay. Essentially you change from:

嘿布赖恩,我在我的应用程序中处理同样的问题。我正在使用 WSDL 生成的代码——实际上我今天再次生成它以确保最新版本。我发现使用 X509 证书签名是最直接的途径。经过几分钟的测试,到目前为止它似乎工作正常。基本上你从:

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

To:

AWSECommerceService service = new AWSECommerceService();
service.ClientCertificates.Add(X509Certificate.CreateFromCertFile(@"path/to/cert.pem"));
// ...then invoke some AWS call
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 at bytesblocks.com posted more details, including how to obtain the X509 certificate Amazon generates for you.

bytesblocks.com 上的 Viper 发布了更多详细信息,包括如何获取亚马逊为您生成的 X509 证书。

EDIT: as the discussion hereindicates, this might not actually sign the request. Will post as I learn more.

编辑:正如这里的讨论所表明的那样,这实际上可能不会签署请求。随着我了解更多,将发布。

EDIT: this doesn't appear to sign the request at all. Instead, it appears to require an https connection, and uses the certificate for SSL client authentication. SSL client authentication is an infrequently used feature of SSL. It would have been nice if the Amazon product advertising API supported it as an authentication mechanism! Unfortunately that doesn't seem to be the case. The evidence is twofold: (1) it's not one of the documented authentication schemes, and (2) it doesn't matter what certificate you specify.

编辑:这似乎根本没有签署请求。相反,它似乎需要 https 连接,并使用证书进行 SSL 客户端身份验证。SSL 客户端身份验证是 SSL 不常用的功能。如果亚马逊产品广告 API 支持它作为身份验证机制,那就太好了!不幸的是,情况似乎并非如此。证据是双重的:(1) 它不是记录在案的身份验证方案之一,以及 (2) 您指定的证书无关紧要。

Some confusion is added by Amazon still not enforcing authentication on requests even after their proclaimed the August 15 2009 deadline. This makes requests appear to pass correctly when the certificate is added, even though it might not add any value.

即使在宣布 2009 年 8 月 15 日截止日期之后,亚马逊仍然没有对请求执行身份验证,这增加了一些混乱。这使得在添加证书时请求似乎可以正确通过,即使它可能不会添加任何值。

Look at Brian Surowiec's answer for a solution that works. I'm leaving this answer here to document the appealing but apparently failed approach, as I can still see it discussed in blogs and Amazon forums.

查看 Brian Surowiec 对有效解决方案的回答。我将这个答案留在这里是为了记录吸引人但显然失败的方法,因为我仍然可以在博客和亚马逊论坛中看到它的讨论。

回答by Brandon

The soap implementation of the signature is kindof nasty. I did it in PHP for use on http://www.apisigning.com/. The trick that I finally figured out was that the Signature, AWSAccessKey, and Timestamp parameters need to go in the SOAP header. Also, the Signature is just a hash of the Operation + timestamp, and doesn't need to include any parameters.

签名的肥皂实现有点讨厌。我在 PHP 中做了它用于http://www.apisigning.com/。我最终发现的诀窍是 Signature、AWSAccessKey 和 Timestamp 参数需要放在 SOAP 标头中。另外,签名只是操作+时间戳的散列,不需要包含任何参数。

I'm not sure how that fits into C#, but thought it might be of some use

我不确定它如何适合 C#,但认为它可能会有用