这是一个较晚的响应,但我有类似的要求,使用 .net core 3.1 调用需要单向 TLS 和 ws-security 的肥皂端点。
首先,添加安全标头非常简单。下面是一个 MessageHeader 实现,它添加了带有时间戳的 Security 标头。如下所示的消息检查器中使用了类 (WsSecurityHeader) 的一个实例。您也可以将此标头烘焙到消息检查器本身中,而不是在消息检查器中使用 WsSecurityHeader,因为消息检查器无论如何都会重写整个soap消息。
using System;
using System.ServiceModel.Channels;
using System.Xml;
namespace MyClient.WsSecurity
{
/// <summary>
/// Adds a WS-Security header to the message, with a Timestamp. The header does not include the message signature,
/// as the framework provides no mechanism to access the message body inside of a MessageHeader implementation.
/// </summary>
public sealed class WsSecurityHeader : MessageHeader
{
public override bool MustUnderstand => true;
public override string Name => "Security";
public const string SoapEnvelopeNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
public const string WsseUtilityNamespaceUrl = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
public const string WsseNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
public override string Namespace => WsseNamespace;
protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteStartElement("wsse", Name, Namespace);
writer.WriteAttributeString("s", "mustUnderstand", SoapEnvelopeNamespace, "1");
writer.WriteXmlnsAttribute("wsse", Namespace);
writer.WriteXmlnsAttribute("wsu", WsseUtilityNamespaceUrl);
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
// Timestamp
writer.WriteStartElement("wsu", "Timestamp", WsseUtilityNamespaceUrl);
writer.WriteAttributeString("wsu", "Id", WsseUtilityNamespaceUrl, "ws-security-timestamp");
writer.WriteStartElement("wsu", "Created", WsseUtilityNamespaceUrl);
writer.WriteValue(DateTimeOffset.Now.ToString("o"));
writer.WriteEndElement();
writer.WriteStartElement("wsu", "Expires", WsseUtilityNamespaceUrl);
writer.WriteValue(DateTimeOffset.Now.AddMinutes(120).ToString("o"));
writer.WriteEndElement();
writer.WriteEndElement(); // Timestamp
}
}
}
为了对消息的 Body 元素进行签名,您需要实现消息检查器。消息检查器让我们可以访问整个消息,包括正文和标题。我们需要修改两者。下面的消息检查器添加了我们的安全标头(WsSecurityHeader 类,如前所示)。我们修改消息的 Body 元素以添加在安全标头中使用的 Id 属性,以标识我们正在签名的元素。然后,我们通过对 Body 元素进行签名来创建签名 xml 元素,并将签名 xml 元素添加到标题中。然后从我们的 XmlDocument 重构整个 soap 消息。
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Xml;
using System.Security.Cryptography.Xml;
using System.IO;
namespace MyClient.WsSecurity
{
/// <summary>
/// Adds a ws-security x509 xml body signature to the outgoing message header. It's annoying that Microsoft contributed to this
/// standard but it's not supported in .NET core.
/// </summary>
public sealed class WsSecurityMessageInspector : IClientMessageInspector
{
public const string BodyIdentifier = "ws-security-body-id"; // This can be whatever xml Id attribute value value we want
public X509Certificate2 X509Certificate { get; }
public WsSecurityMessageInspector() { }
public WsSecurityMessageInspector(X509Certificate2 cert)
{
X509Certificate = cert;
}
public void AfterReceiveReply(ref Message reply, object correlationState) { }
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
// Add the ws-Security header
request.Headers.Add(new WsSecurityHeader());
// Get the entire message as an xml doc, so we can sign the body.
var xml = GetMessageAsString(request);
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = false;
doc.LoadXml(xml);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("soapenv", WsSecurityHeader.SoapEnvelopeNamespace);
nsmgr.AddNamespace("wsse", WsSecurityHeader.WsseNamespace);
// The Body is the element we want to sign.
var body = doc.SelectSingleNode("//soapenv:Body", nsmgr) as XmlElement;
// Add the Id attribute to the Body, for the Reference element URI..
var id = doc.CreateAttribute("wsu", "Id", WsSecurityHeader.WsseUtilityNamespaceUrl);
id.Value = BodyIdentifier;
body.Attributes.Append(id);
// Here we do not adopt the SecurityTokenReference recommendation in the KeyInfo
// section because it is not defined in the XML Signature standard. In lieu of the SecurityTokenReference, we
// add KeyInfoX509Data directly to the KeyInfo node, in accordance with the XML Signature rfc (rfc3075). The SignedXml
// class does not seem to support the SecurityTokenReference, and it's not required.
var signedXml = new SignedXmlWithUriFix(doc);
signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA1Url;
// This cannonicalization method is "recommended" in the ws-security standard, but seems to be required, at least
// by Data Power.
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
// Add the X509 certificate info to the KeyInfo section
var keyInfo = new KeyInfo();
var keyInfoData = new KeyInfoX509Data();
keyInfoData.AddIssuerSerial(X509Certificate.IssuerName.Name, X509Certificate.SerialNumber);
keyInfo.AddClause(keyInfoData);
signedXml.SigningKey = X509Certificate.PrivateKey;
signedXml.KeyInfo = keyInfo;
// Add the reference to the SignedXml object.
Reference reference = new Reference($"#{BodyIdentifier}");
reference.DigestMethod = SignedXml.XmlDsigSHA1Url;
signedXml.AddReference(reference);
// Compute the signature.
signedXml.ComputeSignature();
// Get the Signature element
XmlElement xmlDigitalSignature = signedXml.GetXml();
// Append the Signature element to the XML document's Security header.
XmlNode header = doc.SelectSingleNode("//soapenv:Envelope/soapenv:Header/wsse:Security", nsmgr);
header.AppendChild(doc.ImportNode(xmlDigitalSignature, true));
// Generate a new message from our XmlDocument. We have to be careful here so that the XML is serialized
// with the same whitespace handling (via XmlWriter) as the signed xml (via XmlDocument). A bit sketchy.
var newMessage = CreateMessageFromXmlDocument(request, doc);
request = newMessage;
return null;
}
private Message CreateMessageFromXmlDocument(Message message, XmlDocument doc)
{
MemoryStream ms = new MemoryStream();
using (XmlWriter xmlWriter = XmlWriter.Create(ms, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = false }))
{
doc.WriteTo(xmlWriter);
xmlWriter.Flush();
xmlWriter.Close();
ms.Position = 0;
}
XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(ms, new XmlDictionaryReaderQuotas());
var newMessage = Message.CreateMessage(xdr, int.MaxValue, message.Version);
newMessage.Properties.CopyProperties(message.Properties);
return newMessage;
}
private string GetMessageAsString(Message msg)
{
using (var sw = new StringWriter())
using (var xw = new XmlTextWriter(sw))
{
msg.WriteMessage(xw);
return sw.ToString();
}
}
/// <summary>
/// The SignedXml class chokes on a URI prefixed with "#", so we override the GetIdElement here. The #
/// is allowed by the XML Signature rfc (rfc3075), so this is really a bug fix for SignedXml.
/// </summary>
public class SignedXmlWithUriFix : SignedXml
{
public SignedXmlWithUriFix(XmlDocument xml) : base(xml)
{
}
public SignedXmlWithUriFix(XmlElement xmlElement)
: base(xmlElement)
{
}
public override XmlElement GetIdElement(XmlDocument doc, string id)
{
XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
nsManager.AddNamespace("wsu", WsSecurityHeader.WsseUtilityNamespaceUrl);
return doc.SelectSingleNode($"//*[@wsu:Id=\"{id}\"]", nsManager) as XmlElement;
}
}
}
}
接下来,创建一个行为并添加消息检查器。
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace MyClient.WsSecurity
{
public sealed class WsSecurityHeaderBehavior : IEndpointBehavior
{
public X509Certificate2 X509Certificate { get; }
public WsSecurityHeaderBehavior() { }
public WsSecurityHeaderBehavior(X509Certificate2 cert)
{
X509Certificate = cert;
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
var inspector = new WsSecurityMessageInspector(X509Certificate);
clientRuntime.ClientMessageInspectors.Add(inspector);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
public void Validate(ServiceEndpoint endpoint) { }
}
}
最后,将行为添加到您的 soap 客户端(有用的提示:重新使用相同的绑定实例和 endpointAddress 以允许通道工厂被 .net 核心缓存——至少我记得它是这样工作的)。不要忘记将您的客户端包装在 using 块中,或者在使用后将其丢弃。
var binding = new BasicHttpsBinding();
binding.Security.Mode = BasicHttpsSecurityMode.Transport;
var client= new YourWcfClient(binding, endpointAddress);
// Configure ws-security signing
client.ChannelFactory.Endpoint.EndpointBehaviors.Add(new WsSecurityHeaderBehavior(cert));
此代码已成功用于调用需要单向 TLS 和 ws-security 以及时间戳的 DataPower 端点。可能有更好的方法,但我找不到 .net 核心的任何工作实现。我可能在这里遗漏了一些东西,因为我对 SOAP 和 Ws-Security 的细节不是很熟悉(我只是对自己熟悉到足以将它们组合在一起)。祝你好运!