背景

上一篇文章讲述了如何利用VS(visual studio)引用服务,通过onvif的wsdl文件生成代码框架,从而调用摄像机的onvif接口。本文主要讲述在生成的代码框架的基础上,如何添加鉴权认证信息进行身份认证。


转载引用请务必注明出处,否则我会去百度、google、微信公众平台、工信部举报,后果自负

文章发表的顺序可能有点乱,麻烦根据《基于ONVIF协议的摄像机监控管理平台(1)概述》里面的文章目录进行阅读,抱歉了




一、两种鉴权方式

具体的内容可以去参考我之前写的关于ONVIF鉴权的几篇文章。
这里简要地说一下,分为两种。

  1. http层面上的,通过http 状态码和 http Header实现。



  2. web service层面的。就是在post 上去的xml 文件里面添加 认证信息,也就是 username token digest认证。

二、鉴权需要做的工作

  1. 生成一个基本请求的对象,不带有认证信息。

  2. 修改请求对象的内容。



  3. 发起请求并且等待结果。

三、生成鉴权的信息

  1. 用到的几个参数主要是Created,Nonce,PasswordDigest



  2. c#代码

        private static byte[] buildBytes(string nonce, string createdString, string basedPassword)
        {
            byte[] nonceBytes = System.Convert.FromBase64String(nonce);
            byte[] time = Encoding.UTF8.GetBytes(createdString);
            byte[] pwd = Encoding.UTF8.GetBytes(basedPassword);

            byte[] operand = new byte[nonceBytes.Length + time.Length + pwd.Length];
            Array.Copy(nonceBytes, operand, nonceBytes.Length);
            Array.Copy(time, 0, operand, nonceBytes.Length, time.Length);
            Array.Copy(pwd, 0, operand, nonceBytes.Length + time.Length, pwd.Length);

            return operand;
        }

        /// 计算指定字节数组的哈希值。
        public static byte[] SHAOneHash(byte[] data)
        {
            using (SHA1Managed sha1 = new SHA1Managed())
            {
                var hash = sha1.ComputeHash(data);
                return hash;
            }
        }

        /// 计算公式 Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) ),注意这里的加密不是使用字符串进行加密,而是字节数组
        public static string GetPasswordDigest(string nonce, string createdString, string password)
        {
            byte[] combined = buildBytes(nonce, createdString, password);
            string output = System.Convert.ToBase64String(SHAOneHash(combined));
            return output;
        }

        /// 时间戳
        public static string GetCreated()
        {
            return DateTime.Now.ToString("yyyy-MM-ddThh:mm:ssZ");
        }

        /// 将随机的16位字节数据加密成nonce
        public static string GetNonce()
        {
            byte[] nonce = new byte[16];
            new Random().NextBytes(nonce);
            return System.Convert.ToBase64String(nonce);
        }

四、在调用接口时添加鉴权信息(http digest 摘要认证)

  1. 这个比较简单,不需要上面的代码生成digest

  2. 我手上没有支持http digest的设备可以测试,这里简单贴一下代码吧。

        private void ReferenceTest()
        {
           var messageElement = new TextMessageEncodingBindingElement();
            messageElement.MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None);
            HttpTransportBindingElement httpBinding = new HttpTransportBindingElement();
            httpBinding.AuthenticationScheme = AuthenticationSchemes.Digest;
            CustomBinding bind = new CustomBinding(messageElement, httpBinding);
            EndpointAddress mediaAddress = new EndpointAddress("http://192.168.1.20:80/onvif/media");
    
            ServiceReference2.MediaClient mediaClient = new ServiceReference2.MediaClient(bind, mediaAddress);
            //mediaClient.ClientCredentials.UserName.UserName = "admin";
            //mediaClient.ClientCredentials.UserName.Password = "123456";
    
            mediaClient.ClientCredentials.HttpDigest.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation;
            mediaClient.ClientCredentials.HttpDigest.ClientCredential.UserName = "admin";
            mediaClient.ClientCredentials.HttpDigest.ClientCredential.Password = "123456";
    
            ServiceReference2.Profile[] profiles = mediaClient.GetProfiles();
            string profileToken = profiles[0].token;
            ServiceReference2.MediaUri mediaUri = mediaClient.GetSnapshotUri(profileToken);
             }
    
  3. 如果不支持http层的digest认证的话,程序会报 System.ServiceModel.ProtocolException 异常。

五、在调用接口时添加鉴权信息(web service username token digest 摘要认证)

  1. 先生成一个不带认证信息的请求request

  2. 然后在request的xml中补充相应的header认证信息

  3. 发起请求等待 response

  4. 示例代码(分了几部分的,CustomMessageHeader是添加header认证信息的,)

        public class CustomMessageHeader : MessageHeader
        {
            private const string NAMESPACE_SECURITY_0 = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
            private const string NAMESPACE_SECURITY_1 = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
            public CustomMessageHeader()
            {
            }
            public override string Name
            {
                get { return "wsse:Security"; }
            }
            public override string Namespace
            {
                get { return ""; }
            }
            protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
            {
                string username = "admin";
                string password = "123456";
                string nonce = GetNonce();
                string created = GetCreated();
                string digest = GetPasswordDigest(nonce, created, password);

                writer.WriteAttributeString("xmlns", "wsse", null, NAMESPACE_SECURITY_0);
                writer.WriteAttributeString("xmlns", "wsu", null, NAMESPACE_SECURITY_1);

                writer.WriteStartElement("wsse:UsernameToken");

                writer.WriteStartElement("wsse:Username");
                writer.WriteValue(username);
                writer.WriteEndElement();

                writer.WriteStartElement("wsse:Password");
                //少了这个Type属性,可能会报错
                writer.WriteAttributeString("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
                writer.WriteValue(digest);
                writer.WriteEndElement();

                writer.WriteStartElement("wsse:Nonce");
                writer.WriteValue(nonce);
                writer.WriteEndElement();

                writer.WriteStartElement("wsu:Created");
                writer.WriteValue(created);
                writer.WriteEndElement();

                writer.WriteEndElement();
            }
        }

        public class ClientMessageInspector : IClientMessageInspector
        {
            public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, IClientChannel channel)
            {
                CustomMessageHeader header = new CustomMessageHeader();
                try
                {
                    //会有一些无用的默认header信息,可以删掉
                    request.Headers.RemoveAt(0);
                    request.Headers.RemoveAt(0);
                }
                catch { }
                request.Headers.Add(header);
                return request;
            }
            public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
            {
            }
        }

        public class CustomEndpointBehavior : IEndpointBehavior
        {
            public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
                clientRuntime.ClientMessageInspectors.Add(new ClientMessageInspector());
            }
            public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
            {
            }

            public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
            {
            }

            public void Validate(ServiceEndpoint endpoint)
            {
            }
        }

下面的代码是发起申请的主要逻辑

        private void ReferenceTest()
        {
            var messageElement = new TextMessageEncodingBindingElement();
            messageElement.MessageVersion = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.None);
            HttpTransportBindingElement httpBinding = new HttpTransportBindingElement();
            //httpBinding.AuthenticationScheme = AuthenticationSchemes.None;
            CustomBinding bind = new CustomBinding(messageElement, httpBinding);
            EndpointAddress serviceAddress = new EndpointAddress("http://192.168.110.163:80/onvif/device_service");
            ServiceReference1.DeviceClient deviceClient = new ServiceReference1.DeviceClient(bind, serviceAddress);

            //给每个请求都添加认证信息
            deviceClient.Endpoint.EndpointBehaviors.Add(new CustomEndpointBehavior());
            //不要少了下面这行,会报异常
            ServiceReference1.Device channel = deviceClient.ChannelFactory.CreateChannel();

            ServiceReference1.GetSystemDateAndTimeResponse date = deviceClient.GetSystemDateAndTime(new ServiceReference1.GetSystemDateAndTimeRequest());           Console.WriteLine(date.SystemDateAndTime.UTCDateTime.Date.Month.ToString());
            ServiceReference1.GetCapabilitiesResponse cap = deviceClient.GetCapabilities(new ServiceReference1.GetCapabilitiesRequest());
            Console.WriteLine(cap.Capabilities.Media.XAddr.ToString());

            //上面两个请求不添加认证信息也可以的,下面这个不添加认证信息是无法调用
            ServiceReference1.GetDeviceInformationRequest req = new ServiceReference1.GetDeviceInformationRequest();
            ServiceReference1.GetDeviceInformationResponse rep = deviceClient.GetDeviceInformation(req);
            Console.WriteLine(rep.SerialNumber.ToString());
        }

补充说明

有问题的可以咨询我。有偿咨询,哈哈

后记

有任何的疑问或者想法,或者对本文有质疑或者补充的话,欢迎在留言区评论,期待你的分享!


同时,也欢迎关注扫描屏幕下方关注本人微信公众号或者打赏一下这篇文章。



http://xzh.i3geek.com

爱唠叨的老鱼

爱唠叨的老鱼

技术经理,个人站长,创业者

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据