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

}