ONVIF wsdl 服务:无法验证
ONVIF wsdl service: unable to authenticate
我正在使用 .NET 4(Windows Forms,而不是 WCF)开发 ONVIF 驱动程序。
我在 visual studio 开始将 WSDL 文件作为服务导入。
所以我可以通过这种方式向设备发送命令:
HttpTransportBindingElement httpTransportBindingElement = new HttpTransportBindingElement();
[...]
TextMessageEncodingBindingElement messegeElement = new TextMessageEncodingBindingElement();
[...]
CustomBinding binding = new CustomBinding(messegeElement, httpTransportBindingElement);
[...]
EndpointAddress serviceAddress = new EndpointAddress(url);
DeviceClient deviceClient = new DeviceClient(binding, serviceAddress);
Device channel = deviceClient.ChannelFactory.CreateChannel();
DeviceServiceCapabilities dsc = channel.GetServiceCapabilities();
但我无法管理 HTTP 摘要身份验证。我花了几天时间搜索 google 示例和解决方案,但唯一的方法似乎是手写 XML 代码。没有像这样的干净解决方案:
deviceClient.ChannelFactory.Credentials.HttpDigest.ClientCredential.UserName = USERNAME;
deviceClient.ChannelFactory.Credentials.HttpDigest.ClientCredential.Password = digestPassword;
(这行不通)?
首先你应该安装 Microsoft.Web.Services3 包。 (查看 > 其他 windows> 包管理器控制台)。然后,您必须将摘要行为添加到端点。代码的第一部分是 PasswordDigestBehavior class,之后用于连接到 ONVIF 设备服务。
public class PasswordDigestBehavior : IEndpointBehavior
{
public String Username { get; set; }
public String Password { get; set; }
public PasswordDigestBehavior(String username, String password)
{
this.Username = username;
this.Password = password;
}
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
// do nothing
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
//clientRuntime.MessageInspectors.Add(new PasswordDigestMessageInspector(this.Username, this.Password));
clientRuntime.MessageInspectors.Add(new PasswordDigestMessageInspector(this.Username, this.Password));
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
throw new NotImplementedException();
}
public void Validate(ServiceEndpoint endpoint)
{
// do nothing...
}
}
public class PasswordDigestMessageInspector : IClientMessageInspector
{
public String Username { get; set; }
public String Password { get; set; }
public PasswordDigestMessageInspector(String username, String password)
{
this.Username = username;
this.Password = password;
}
public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
// do nothing
}
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
// Use the WSE 3.0 security token class
var option = PasswordOption.SendHashed;
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
option = PasswordOption.SendPlainText;
UsernameToken token = new UsernameToken(this.Username, this.Password, option);
// Serialize the token to XML
XmlDocument xmlDoc = new XmlDocument();
XmlElement securityToken = token.GetXml(xmlDoc);
// find nonce and add EncodingType attribute for BSP compliance
XmlNamespaceManager nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsMgr.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
XmlNodeList nonces = securityToken.SelectNodes("//wsse:Nonce", nsMgr);
XmlAttribute encodingAttr = xmlDoc.CreateAttribute("EncodingType");
encodingAttr.Value = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary";
if (nonces.Count > 0)
{
nonces[0].Attributes.Append(encodingAttr);
//nonces[0].Attributes[0].Value = "foo";
}
//
MessageHeader securityHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", securityToken, false);
request.Headers.Add(securityHeader);
// complete
return Convert.DBNull;
}
}
这是使用方法:
var endPointAddress = new EndpointAddress("http://DEVICE_IPADDRESS/onvif/device_service");
var httpTransportBinding = new HttpTransportBindingElement { AuthenticationScheme = AuthenticationSchemes.Digest };
var textMessageEncodingBinding = new TextMessageEncodingBindingElement { MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None) };
var customBinding = new CustomBinding(textMessageEncodingBinding, httpTransportBinding);
var passwordDigestBehavior = new PasswordDigestBehavior(USERNAME, PASSWORD);
var deviceService = new DeviceClient(customBinding, endPointAddress);
deviceService.Endpoint.Behaviors.Add(passwordDigestBehavior);
对于未来的读者,我终于能够在不使用 WSE 3.0 的情况下执行这两种类型的身份验证。
这是部分代码(简而言之),基于 IClientMessageInspector 接口(您可以找到很多基于此接口的其他示例):
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
if (HTTPDigestAuthentication)
{
string digestHeader = string.Format("Digest username=\"{0}\",realm=\"{1}\",nonce=\"{2}\",uri=\"{3}\"," +
"cnonce=\"{4}\",nc={5:00000000},qop={6},response=\"{7}\",opaque=\"{8}\"",
_username, realm, nonce, new Uri(this.URI).AbsolutePath, cnonce, counter, qop, digestResponse, opaque);
HttpRequestMessageProperty httpRequest = new HttpRequestMessageProperty();
httpRequest.Headers.Add("Authorization", digestHeader);
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequest);
return Convert.DBNull;
}
else if (UsernametokenAuthorization)
{
string headerText = "<wsse:UsernameToken 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\">" +
"<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\">" + digestPassword + "</wsse:Password>" +
"<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" + Convert.ToBase64String(nonce) + "</wsse:Nonce>" +
"<wsu:Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" + created + "</wsu:Created>" +
"</wsse:UsernameToken>";
XmlDocument MyDoc = new XmlDocument();
MyDoc.LoadXml(headerText);
MessageHeader myHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", MyDoc.DocumentElement, false);
request.Headers.Add(myHeader);
return Convert.DBNull;
}
return request;
}
这个class应该可以替换WSE UserNameToken 对象并移除对WSE 的依赖。它还使得在 IClientInspector 中搜索和修复随机数变得不必要。我只在一台相机上测试过它,并且只使用散列密码。 YMMV。
public enum PasswordOption
{
SendPlain = 0,
SendHashed = 1,
SendNone = 2
}
public class UsernameToken
{
private string Username;
private string Password;
private PasswordOption PwdOption;
public UsernameToken(string username, string password, PasswordOption option)
{
Username = username;
Password = password;
PwdOption = option;
}
public XmlElement GetXml(XmlDocument xmlDoc)
{
string wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
string wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
XmlDocument doc = xmlDoc;
//XmlElement securityEl = doc.CreateElement("Security", wsse);
XmlElement usernameTokenEl = doc.CreateElement("wsse", "UsernameToken", wsse);
XmlAttribute a = doc.CreateAttribute("wsu", "Id", wsu);
usernameTokenEl.SetAttribute("xmlns:wsse", wsse);
usernameTokenEl.SetAttribute("xmlns:wsu", wsu);
a.InnerText = "SecurityToken-" + Guid.NewGuid().ToString();
usernameTokenEl.Attributes.Append(a);
//Username
XmlElement usernameEl = doc.CreateElement("wsse:Username", wsse);
usernameEl.InnerText = Username;
usernameTokenEl.AppendChild(usernameEl);
//Password
XmlElement pwdEl = doc.CreateElement("wsse:Password", wsse);
switch (PwdOption)
{
case PasswordOption.SendHashed:
//Nonce+Create+Password
pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
string created = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
byte[] nonce = GenerateNonce(16);
byte[] pwdBytes = Encoding.ASCII.GetBytes(Password);
byte[] createdBytes = Encoding.ASCII.GetBytes(created);
byte[] pwdDigest = new byte[nonce.Length + pwdBytes.Length + createdBytes.Length];
Array.Copy(nonce, pwdDigest, nonce.Length);
Array.Copy(createdBytes, 0, pwdDigest, nonce.Length, createdBytes.Length);
Array.Copy(pwdBytes, 0, pwdDigest, nonce.Length + createdBytes.Length, pwdBytes.Length);
pwdEl.InnerText = ToBase64(SHA1Hash(pwdDigest));
usernameTokenEl.AppendChild(pwdEl);
//Nonce
XmlElement nonceEl = doc.CreateElement("wsse:Nonce", wsse);
nonceEl.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
nonceEl.InnerText = ToBase64(nonce);
usernameTokenEl.AppendChild(nonceEl);
//Created
XmlElement createdEl = doc.CreateElement("wsu:Created", wsu);
createdEl.InnerText = created;
usernameTokenEl.AppendChild(createdEl);
break;
case PasswordOption.SendNone:
pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
pwdEl.InnerText = "";
usernameTokenEl.AppendChild(pwdEl);
break;
case PasswordOption.SendPlain:
pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
pwdEl.InnerText = Password;
usernameTokenEl.AppendChild(pwdEl);
break;
}
return usernameTokenEl;
}
private byte[] GenerateNonce(int bytes)
{
byte[] output = new byte[bytes];
Random r = new Random(DateTime.Now.Millisecond);
r.NextBytes(output);
return output;
}
private static byte[] SHA1Hash(byte[] input)
{
SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
return sha1Hasher.ComputeHash(input);
}
private static string ToBase64(byte[] input)
{
return Convert.ToBase64String(input);
}
}
}
我正在使用 .NET 4(Windows Forms,而不是 WCF)开发 ONVIF 驱动程序。 我在 visual studio 开始将 WSDL 文件作为服务导入。 所以我可以通过这种方式向设备发送命令:
HttpTransportBindingElement httpTransportBindingElement = new HttpTransportBindingElement();
[...]
TextMessageEncodingBindingElement messegeElement = new TextMessageEncodingBindingElement();
[...]
CustomBinding binding = new CustomBinding(messegeElement, httpTransportBindingElement);
[...]
EndpointAddress serviceAddress = new EndpointAddress(url);
DeviceClient deviceClient = new DeviceClient(binding, serviceAddress);
Device channel = deviceClient.ChannelFactory.CreateChannel();
DeviceServiceCapabilities dsc = channel.GetServiceCapabilities();
但我无法管理 HTTP 摘要身份验证。我花了几天时间搜索 google 示例和解决方案,但唯一的方法似乎是手写 XML 代码。没有像这样的干净解决方案:
deviceClient.ChannelFactory.Credentials.HttpDigest.ClientCredential.UserName = USERNAME;
deviceClient.ChannelFactory.Credentials.HttpDigest.ClientCredential.Password = digestPassword;
(这行不通)?
首先你应该安装 Microsoft.Web.Services3 包。 (查看 > 其他 windows> 包管理器控制台)。然后,您必须将摘要行为添加到端点。代码的第一部分是 PasswordDigestBehavior class,之后用于连接到 ONVIF 设备服务。
public class PasswordDigestBehavior : IEndpointBehavior
{
public String Username { get; set; }
public String Password { get; set; }
public PasswordDigestBehavior(String username, String password)
{
this.Username = username;
this.Password = password;
}
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
// do nothing
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
//clientRuntime.MessageInspectors.Add(new PasswordDigestMessageInspector(this.Username, this.Password));
clientRuntime.MessageInspectors.Add(new PasswordDigestMessageInspector(this.Username, this.Password));
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
throw new NotImplementedException();
}
public void Validate(ServiceEndpoint endpoint)
{
// do nothing...
}
}
public class PasswordDigestMessageInspector : IClientMessageInspector
{
public String Username { get; set; }
public String Password { get; set; }
public PasswordDigestMessageInspector(String username, String password)
{
this.Username = username;
this.Password = password;
}
public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
// do nothing
}
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
// Use the WSE 3.0 security token class
var option = PasswordOption.SendHashed;
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
option = PasswordOption.SendPlainText;
UsernameToken token = new UsernameToken(this.Username, this.Password, option);
// Serialize the token to XML
XmlDocument xmlDoc = new XmlDocument();
XmlElement securityToken = token.GetXml(xmlDoc);
// find nonce and add EncodingType attribute for BSP compliance
XmlNamespaceManager nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsMgr.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
XmlNodeList nonces = securityToken.SelectNodes("//wsse:Nonce", nsMgr);
XmlAttribute encodingAttr = xmlDoc.CreateAttribute("EncodingType");
encodingAttr.Value = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary";
if (nonces.Count > 0)
{
nonces[0].Attributes.Append(encodingAttr);
//nonces[0].Attributes[0].Value = "foo";
}
//
MessageHeader securityHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", securityToken, false);
request.Headers.Add(securityHeader);
// complete
return Convert.DBNull;
}
}
这是使用方法:
var endPointAddress = new EndpointAddress("http://DEVICE_IPADDRESS/onvif/device_service");
var httpTransportBinding = new HttpTransportBindingElement { AuthenticationScheme = AuthenticationSchemes.Digest };
var textMessageEncodingBinding = new TextMessageEncodingBindingElement { MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None) };
var customBinding = new CustomBinding(textMessageEncodingBinding, httpTransportBinding);
var passwordDigestBehavior = new PasswordDigestBehavior(USERNAME, PASSWORD);
var deviceService = new DeviceClient(customBinding, endPointAddress);
deviceService.Endpoint.Behaviors.Add(passwordDigestBehavior);
对于未来的读者,我终于能够在不使用 WSE 3.0 的情况下执行这两种类型的身份验证。 这是部分代码(简而言之),基于 IClientMessageInspector 接口(您可以找到很多基于此接口的其他示例):
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
if (HTTPDigestAuthentication)
{
string digestHeader = string.Format("Digest username=\"{0}\",realm=\"{1}\",nonce=\"{2}\",uri=\"{3}\"," +
"cnonce=\"{4}\",nc={5:00000000},qop={6},response=\"{7}\",opaque=\"{8}\"",
_username, realm, nonce, new Uri(this.URI).AbsolutePath, cnonce, counter, qop, digestResponse, opaque);
HttpRequestMessageProperty httpRequest = new HttpRequestMessageProperty();
httpRequest.Headers.Add("Authorization", digestHeader);
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequest);
return Convert.DBNull;
}
else if (UsernametokenAuthorization)
{
string headerText = "<wsse:UsernameToken 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\">" +
"<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\">" + digestPassword + "</wsse:Password>" +
"<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" + Convert.ToBase64String(nonce) + "</wsse:Nonce>" +
"<wsu:Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" + created + "</wsu:Created>" +
"</wsse:UsernameToken>";
XmlDocument MyDoc = new XmlDocument();
MyDoc.LoadXml(headerText);
MessageHeader myHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", MyDoc.DocumentElement, false);
request.Headers.Add(myHeader);
return Convert.DBNull;
}
return request;
}
这个class应该可以替换WSE UserNameToken 对象并移除对WSE 的依赖。它还使得在 IClientInspector 中搜索和修复随机数变得不必要。我只在一台相机上测试过它,并且只使用散列密码。 YMMV。
public enum PasswordOption
{
SendPlain = 0,
SendHashed = 1,
SendNone = 2
}
public class UsernameToken
{
private string Username;
private string Password;
private PasswordOption PwdOption;
public UsernameToken(string username, string password, PasswordOption option)
{
Username = username;
Password = password;
PwdOption = option;
}
public XmlElement GetXml(XmlDocument xmlDoc)
{
string wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
string wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
XmlDocument doc = xmlDoc;
//XmlElement securityEl = doc.CreateElement("Security", wsse);
XmlElement usernameTokenEl = doc.CreateElement("wsse", "UsernameToken", wsse);
XmlAttribute a = doc.CreateAttribute("wsu", "Id", wsu);
usernameTokenEl.SetAttribute("xmlns:wsse", wsse);
usernameTokenEl.SetAttribute("xmlns:wsu", wsu);
a.InnerText = "SecurityToken-" + Guid.NewGuid().ToString();
usernameTokenEl.Attributes.Append(a);
//Username
XmlElement usernameEl = doc.CreateElement("wsse:Username", wsse);
usernameEl.InnerText = Username;
usernameTokenEl.AppendChild(usernameEl);
//Password
XmlElement pwdEl = doc.CreateElement("wsse:Password", wsse);
switch (PwdOption)
{
case PasswordOption.SendHashed:
//Nonce+Create+Password
pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
string created = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
byte[] nonce = GenerateNonce(16);
byte[] pwdBytes = Encoding.ASCII.GetBytes(Password);
byte[] createdBytes = Encoding.ASCII.GetBytes(created);
byte[] pwdDigest = new byte[nonce.Length + pwdBytes.Length + createdBytes.Length];
Array.Copy(nonce, pwdDigest, nonce.Length);
Array.Copy(createdBytes, 0, pwdDigest, nonce.Length, createdBytes.Length);
Array.Copy(pwdBytes, 0, pwdDigest, nonce.Length + createdBytes.Length, pwdBytes.Length);
pwdEl.InnerText = ToBase64(SHA1Hash(pwdDigest));
usernameTokenEl.AppendChild(pwdEl);
//Nonce
XmlElement nonceEl = doc.CreateElement("wsse:Nonce", wsse);
nonceEl.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
nonceEl.InnerText = ToBase64(nonce);
usernameTokenEl.AppendChild(nonceEl);
//Created
XmlElement createdEl = doc.CreateElement("wsu:Created", wsu);
createdEl.InnerText = created;
usernameTokenEl.AppendChild(createdEl);
break;
case PasswordOption.SendNone:
pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
pwdEl.InnerText = "";
usernameTokenEl.AppendChild(pwdEl);
break;
case PasswordOption.SendPlain:
pwdEl.SetAttribute("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText");
pwdEl.InnerText = Password;
usernameTokenEl.AppendChild(pwdEl);
break;
}
return usernameTokenEl;
}
private byte[] GenerateNonce(int bytes)
{
byte[] output = new byte[bytes];
Random r = new Random(DateTime.Now.Millisecond);
r.NextBytes(output);
return output;
}
private static byte[] SHA1Hash(byte[] input)
{
SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
return sha1Hasher.ComputeHash(input);
}
private static string ToBase64(byte[] input)
{
return Convert.ToBase64String(input);
}
}
}