c# - 如何在 C# 中为 SOAP 请求正确生成安全标头?
问题描述
我正在尝试使用 C# 使用 SOAP 服务,但我正在努力处理 Outgoing WS-Security 配置。
到目前为止我所做的:
- 在 VS 2019 中创建了一个服务引用并扩展了生成的 Client 类以在其中传递我的配置
- 创建了一个自定义端点行为:
this.ChannelFactory.Endpoint.EndpointBehaviors.Add(new WsSecurityEndpointBehavior(username, password, timeout));
// ...
public class WsSecurityEndpointBehavior : IEndpointBehavior
{
private readonly string _password;
private readonly TimeSpan _timeout;
private readonly string _username;
public WsSecurityEndpointBehavior(string username, string password, TimeSpan timeout)
{
this._username = username;
this._password = password;
this._timeout = timeout;
}
void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.ClientMessageInspectors.Add(new WsSecurityMessageInspector(_username, _password, _timeout));
}
void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
void IEndpointBehavior.Validate(ServiceEndpoint endpoint)
{
}
}
- 创建了一个 ClientMessageInspector:
public class WsSecurityMessageInspector : IClientMessageInspector
{
private readonly string _password;
private readonly TimeSpan _timeout;
private readonly string _username;
public WsSecurityMessageInspector(string username, string password, TimeSpan timeout)
{
this._username = username;
this._password = password;
this._timeout = timeout;
}
object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
{
var header = new Security
{
UsernameToken =
{
Password = new Password
{
Value = _password,
Type =
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
},
Username = _username
},
Timestamp =
{
Created = DateTime.UtcNow.ToString("yyyy-MM-ddThh:mm:ss.fffZ"),
Expires = DateTime.UtcNow.Add(_timeout).ToString("yyyy-MM-ddThh:mm:ss.fffZ")
}
};
request.Headers.Add(header);
return null;
}
void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
{
}
}
- 为 xml 字段添加了 poco 类:
public class Password
{
[XmlAttribute] public string Type { get; set; }
[XmlText] public string Value { get; set; }
}
public class UsernameToken
{
[XmlElement] public string Username { get; set; }
[XmlElement] public Password Password { get; set; }
}
public class Timestamp
{
[XmlElement] public string Created { get; set; }
[XmlElement] public string Expires { get; set; }
}
- 最后实现了一个安全消息头:
public class Security : MessageHeader
{
public Security()
{
UsernameToken = new UsernameToken();
Timestamp = new Timestamp();
}
public UsernameToken UsernameToken { get; set; }
public Timestamp Timestamp { get; set; }
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 Name => GetType().Name;
public override string Namespace => WsseNamespace;
public override bool MustUnderstand => true;
protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteStartElement("wsse", Name, Namespace);
//writer.WriteAttributeString("s", "mustUnderstand", SoapEnvelopeNamespace, "1"); // - doesn't work either way
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, "TS-" + Guid.NewGuid());
writer.WriteStartElement("wsu", "Created", WsseUtilityNamespaceUrl);
writer.WriteValue(Timestamp.Created);
writer.WriteEndElement();
writer.WriteStartElement("wsu", "Expires", WsseUtilityNamespaceUrl);
writer.WriteValue(Timestamp.Expires);
writer.WriteEndElement();
writer.WriteEndElement(); // Timestamp
// username token
writer.WriteStartElement("wsse", "UsernameToken", Namespace);
writer.WriteAttributeString("wsu", "Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "UsernameToken-" + Guid.NewGuid());
// Username
writer.WriteStartElement("wsse", "Username", Namespace);
writer.WriteValue(UsernameToken.Username);
writer.WriteEndElement();
var nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));
var createdStr = Timestamp.Created;
var pwd = GetSHA1String(nonce + createdStr + UsernameToken.Password.Value);
// Password
writer.WriteStartElement("wsse", "Password", Namespace);
writer.WriteAttributeString("Type", UsernameToken.Password.Type); //PasswordDigest
writer.WriteValue(pwd);
writer.WriteEndElement();
writer.WriteStartElement("wsse", "Nonce", Namespace);
writer.WriteAttributeString("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
writer.WriteValue(nonce);
writer.WriteEndElement();
writer.WriteStartElement("wsu", "Created", WsseUtilityNamespaceUrl);
writer.WriteValue(createdStr);
writer.WriteEndElement();
// username token
writer.WriteEndElement();
}
private static string GetSHA1String(string phrase)
{
var sha1Hasher = new SHA1CryptoServiceProvider();
var hashedDataBytes = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(phrase));
return Convert.ToBase64String(hashedDataBytes);
}
}
我在结果中的 XML 示例:
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="TS-2b7092f4-8f62-40b5-8f7e-ab774ef967aa">
<wsu:Created>2021-05-17T10:20:16.913Z</wsu:Created>
<wsu:Expires>2021-05-17T10:25:16.913Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id="UsernameToken-8ee97b0e-cbdf-465b-97b0-8e5194f2f4a9">
<wsse:Username>username</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">base64_described_above</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">MGNiNDUxMTYtYjBlMy00ZmQzLTgwYTUtODgxZGY2NmUyMDI0</wsse:Nonce>
<wsu:Created>2021-05-17T10:20:16.913Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
我能够让它在 Soap UI 中工作,但是通过这个 C# 实现,我得到了ns1:SecurityError
. 我究竟做错了什么?
提前致谢。
----更新---- 完整的请求 XML:
<?xml version="1.0" encoding="utf-16"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1"
xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">[ActionICall]</Action>
<h:RequestContext xmlns="http://hidden-for-security/webservices/v1/messages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:h="http://hidden-for-security/webservices/v1/messages">
<Id>f7ed31dc-b7ad-4a5d-ae17-c86663bbb2da</Id>
<Language>en</Language>
</h:RequestContext>
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="TS-ceb077e4-3255-405b-aac7-d7ae9bf1b884">
<wsu:Created>2021-05-17T12:01:11.852Z</wsu:Created>
<wsu:Expires>2021-05-17T12:06:11.852Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id="UsernameToken-1373fa47-6f2a-4d44-87d5-5270132f9830">
<wsse:Username>username</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">calculated-password-in-base64</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">MjkxY2VkY2MtZjA4ZC00NmQ0LWFkMjUtYWQ4ZGU3ODFmNGVm</wsse:Nonce>
<wsu:Created>2021-05-17T12:01:11.852Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ActionICallRequest xmlns="http://hidden-for-security/webservices/v1/messages">
<UniqueNumber xmlns="http://hidden-for-security/webservices/v1/datamodel">659648795</UniqueNumber>
</ActionICallRequest>
</s:Body>
</s:Envelope>
我得到的回应:
<?xml version="1.0" encoding="utf-16"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" />
<soap:Body>
<soap:Fault>
<faultcode xmlns:ns1="http://ws.apache.org/wss4j">ns1:SecurityError</faultcode>
<faultstring>A security error was encountered when verifying the message</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
解决方案
推荐阅读
- javascript - 我需要有关如何从节点 js 在 sql server 中插入的帮助
- python - 将控制权交还给运输工具
- metal - Metal Compute 与 ARM Neon
- c++ - 在没有活动异常的情况下调用终止(抛出 catch all 表达式)
- r - R中逻辑回归的模型框架默认错误变量长度不同
- python-3.x - 这两个python3版本有什么区别?
- sql - 如何使用 Python/Django 更新/保存数据库中的 oracle 同义词
- node.js - 机器人框架 v4 相当于 Session.delay()?
- unique-id - 如何禁用 Chrome 元素唯一 ID 检查
- javascript - 如何从 Google Apps 脚本中的 URL 获取音频对象?