背景

上一篇文章主要讲述了ONVIF和SOAP鉴权认证的流程和基本原理,以及Digest摘要认证和Token令牌认证这两种认证的流程。本文主要讲述如何用工具去模拟这个流程。

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


一、认证怎么选择

上一篇关于鉴权的文章里写到了,鉴权分为http层和web service层的两种。实际上,这两种协议都是支持的,当然了,山寨厂的设备支持不支持我就不太清楚了。
摄像头会根据用户的请求去决定怎么进行认证的操作,具体的流程图如下所示。
ONVIF鉴权认证流程图



需要注意的是,如果同时用了两种认证,请确保这两种都能正确运行,否则其中一个出错都会导致认证失败。

二、http层的认证(Digest)

先引用一下上一篇文章的内容:



  1. http认证的流程
    • a.客户端发起http请求
    • b.服务器响应401 Unauthorized,WWW-Authenticate指定认证算法,realm指定安全域
    • c.客户端重新发起请求,Authorization指定用户名和密码信息
    • d.服务器认证成功,响应200,可选Authentication-Info
  2. 所谓Digest摘要认证,就是在上述第2步时,服务器会返回一个随机的字符串nonce,而客户端则需要使用这个随机字符串进行一系列运算,得到一个摘要,发送给服务器进行认证。
    摘要的计算方法为:MD5(HA1:nonce:HA2),其中HA1=MD5(username:realm:password),HA2=MD5(method:digestURI)

这个可能不太准确,具体的算法可以参考一下官方的文档。


  1. 本来想给出一段代码测试一下这个方法的,无奈我手上的所有摄像头,都不支持这个认证。



  2. 也就是我发起第一次接口请求的时候,如果不带认证信息的话,服务器本该是需要返回给我一个401信息的,同时还会携带nonce等其它的一些信息。然后我们需要用这些信息进行一个运算,把运算结果加到http的Header里面,再发起一次请求(也就是说这次请求已经是带有鉴权信息)。如果携带的信息是正确的,那么就可以正常的调用ONVIF接口。但是我的摄像头没有返回401。

  3. 要知道自己的摄像头是否支持 http digest认证的话,可以用 ONVIF device test tool测试一下。这样子得出的结论,比自己判断会更加可靠一点。



  4. 具体方法是下载安装ONVIF device test tool,然后填上摄像头的信息不填账号密码,在Diagnostic那个页面那里,只选中 Security Test Cases。点击测试就可以得到结果了。

  5. ONVIF device test tool测试分为 Username Token 和 Http Digest两步。如果Http Digest那里显示没有收到401返回码,那就代表不支持Http Digest这种鉴权认证了。

三、web service层的认证(Username Token)

  1. 所谓web service层的认证,也就是在调用接口的时候,把username等信息,附加到http post的 XML里面。

  2. 如果是直接用 gSOAP 或者其它现成的ONVIF框架的话,可以直接调用 auth 函数或者方法,填上用户名和密码,就会自动帮你完成一系列的计算过程,不需要自己运算的。

  3. 先贴一段我用 wireshark 抓包抓出来的XML吧(这是ONVIF device test tool发送给摄像头的)。

    <?xml  version="1.0"  encoding="utf-8" ?>
    <s:Envelope
        xmlns:s="http://www.w3.org/2003/05/soap-envelope">
        <s:Header>
            <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">
                <wsse:UsernameToken>
                    <wsse:Username>
                        admin
                        </wsse:Username>
                    <wsse:Password
                        Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">
                        l1udcA9mk+pqg1CwJnXOaCyDZao=
                        </wsse:Password>
                    <wsse:Nonce>
                        MCYIbkBwNkalKcL+VzG92A==
                        </wsse:Nonce>
                    <wsu:Created>
                        2017-12-28T09:48:23Z
                        </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">
            <GetDeviceInformation
                xmlns="http://www.onvif.org/ver10/device/wsdl"/>
            </s:Body>
        </s:Envelope>
    
  4. 简单讲一下这段xml。
    • GetDeviceInformation表明这是在调用获取设备信息的ONVIF接口。
    • 如果没有携带认证信息,大部分摄像头(某些山寨的除外)会拒绝访问并且返回 not authed的信息的。
    • 携带的认证信息,是在 xml的Header那里。
    • 包括 username,password,nonce,created这几个。
    • 我们可以直接使用上面这段代码,但是很明显是会报错的。
    • 不过我们只需要把username,password,nonce,created替换掉就可以了,其他格式是不需要变动的。
  5. 怎么去计算这些信息
    按照官方文档的说明,计算方法如下。
    Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )
    注意这里的加密不是使用字符串进行加密,而是字节数组

有需要的,可以google一下《ONVIF Appicaltion Programmer’s Guide》,里面会有细节。
下面这个地址不一定能用。
《ONVIF_WG-APG-Application_Programmers_Guide-1.pdf》

  • username就是用户名,直接填就好了
  • nonce是客户端随机生成的
  • created是UtcTime时间,格式可以参照一下上面那段xml里面的内容
  • password就是Digest摘要,用上面那条Digest公式计算。

四、具体代码实现

关于 http digest的实现,因为我手上没支持的设备,就不发代码了。
这里就给出username token 这种方式的实现代码吧。

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

        private async void HttpAuthTest()
        {
            //用官方的example,测试一下我们的摘要算法是否有问题
            if ("tuOSpGlFlIXsozq4HFNeeGeFLEI=" == GetPasswordDigest("LKqI6G/AikKCQrN0zqZFlg==", "2010-09-16T07:50:45Z", "userpassword"))
            {
                Console.WriteLine("Test Passed!");
            }
            else
            {
                Console.WriteLine("Test Failed!");
                return ;
            }
            //ONVIF服务所在的地址
            var url = "http://192.168.110.163/onvif/device_service";
            //创建一个http请求
            using (var client = new HttpClient())
            {
                //清楚http请求的所有默认Header,在Header中添加SOAPAction
                client.DefaultRequestHeaders.Clear();
                client.DefaultRequestHeaders.Add("SOAPAction", "https://www.onvif.org/ver10/device/wsdl/GetDeviceInformation");
                //这段xml字符串是复制过来的,用它生成一个xml结构,然后选择性的修改一部分内容
                String xmlStr = "<?xml version=\"1.0\" encoding=\"utf - 8\"?><s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"><s:Header><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\"><wsse:UsernameToken><wsse:Username>admin</wsse:Username><wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">59zlfHOLcoptDNzx2MVvsIiqcow=</wsse:Password><wsse:Nonce>hlCkYZgzndYSXPcrr+RRXg==</wsse:Nonce><wsu:Created>2017-12-28T09:57:22Z</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\"><GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/></s:Body></s:Envelope>";
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(xmlStr);
                XmlNode username = doc.GetElementsByTagName("wsse:Username").Item(0);
                XmlNode digest = doc.GetElementsByTagName("wsse:Password").Item(0);
                XmlNode nonce = doc.GetElementsByTagName("wsse:Nonce").Item(0);
                XmlNode created = doc.GetElementsByTagName("wsu:Created").Item(0);
                Console.WriteLine(username.InnerText.ToString());
                Console.WriteLine(digest.InnerText.ToString());
                Console.WriteLine(nonce.InnerText.ToString());
                Console.WriteLine(created.InnerText.ToString());
                //生成请求和相应的参数
                username.InnerText = "admin";
                nonce.InnerText = GetNonce();
                created.InnerText = GetCreated();
                digest.InnerText = GetPasswordDigest(nonce.InnerText, created.InnerText, "123456");
                Console.WriteLine(doc.OuterXml);
                //设置一下 http post的格式
                var content = new StringContent(doc.OuterXml, Encoding.UTF8, "application/xml");
                //发起请求
                var response = await client.PostAsync(url, content);
                //查看响应信息
                var responseString = await response.Content.ReadAsStringAsync();
                Console.WriteLine(responseString.ToString());
            }
        }

如果鉴权失败,那么收到摄像机返回来的信息跟下面的差不多。

<?xml version="1.0" encoding="utf-8"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:xmime4="http://www.w3.org/2004/11/xmlmime" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:tes-e="http://www.onvif.org/ver10/events/wsdl/EventBinding" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tes-nc="http://www.onvif.org/ver10/events/wsdl/NotificationConsumerBinding" xmlns:tes-np="http://www.onvif.org/ver10/events/wsdl/NotificationProducerBinding" xmlns:tes-sm="http://www.onvif.org/ver10/events/wsdl/SubscriptionManagerBinding" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:xmime="http://www.w3.org/2004/06/xmlmime" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" 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" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tan-ae="http://www.onvif.org/ver20/analytics/wsdl/AnalyticsEngineBinding" xmlns:tan-re="http://www.onvif.org/ver20/analytics/wsdl/RuleEngineBinding" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:tpl="http://www.onvif.org/ver10/plus/wsdl" xmlns:tplt="http://www.onvif.org/ver10/plus/schema">
  <SOAP-ENV:Header/>
  <SOAP-ENV:Body>
    <SOAP-ENV:Fault>
      <SOAP-ENV:Code>
        <SOAP-ENV:Value>SOAP-ENV:Sender</SOAP-ENV:Value>
        <SOAP-ENV:Subcode>
          <SOAP-ENV:Value>ter:NotAuthorized</SOAP-ENV:Value>
        </SOAP-ENV:Subcode>
      </SOAP-ENV:Code>
      <SOAP-ENV:Reason>
        <SOAP-ENV:Text xml:lang="en">Sender not Authorized</SOAP-ENV:Text>
      </SOAP-ENV:Reason>
    </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

如果鉴权成功,那么收到摄像机返回来的信息跟下面的差不多,可以查看信息。

<?xml version="1.0" encoding="utf-8"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xop="http://www.w3.org/2004/08/xop/include" xmlns:xmime4="http://www.w3.org/2004/11/xmlmime" xmlns:wsa5="http://www.w3.org/2005/08/addressing" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:tes-e="http://www.onvif.org/ver10/events/wsdl/EventBinding" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tes-nc="http://www.onvif.org/ver10/events/wsdl/NotificationConsumerBinding" xmlns:tes-np="http://www.onvif.org/ver10/events/wsdl/NotificationProducerBinding" xmlns:tes-sm="http://www.onvif.org/ver10/events/wsdl/SubscriptionManagerBinding" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:xmime="http://www.w3.org/2004/06/xmlmime" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" 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" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tan-ae="http://www.onvif.org/ver20/analytics/wsdl/AnalyticsEngineBinding" xmlns:tan-re="http://www.onvif.org/ver20/analytics/wsdl/RuleEngineBinding" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:tpl="http://www.onvif.org/ver10/plus/wsdl" xmlns:tplt="http://www.onvif.org/ver10/plus/schema">
  <SOAP-ENV:Header/>
  <SOAP-ENV:Body>
    <tds:GetDeviceInformationResponse>
      <tds:Manufacturer>UNIVIEW</tds:Manufacturer>
      <tds:Model>IPC244S-IR5-F36-DT</tds:Model>
      <tds:FirmwareVersion>IPC_G6102-B5011P10D1604</tds:FirmwareVersion>
      <tds:SerialNumber>210235C1X8A166000091</tds:SerialNumber>
      <tds:HardwareId>IPC244S-IR5-F36-DT</tds:HardwareId>
    </tds:GetDeviceInformationResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

五、怎么测试代码的正确性

这里的测试,主要指的是怎么计算password的Digest。如果运行代码失败的话,给一个官方的例子大家测试一下自己的算法是否正确。

再次重申一次!!!!!!!!!!!!
注意这里的加密不是使用字符串进行加密,而是字节数组
使用其它语言开发的,可以参照我上面的代码稍作修改即可

  • Nonce – LKqI6G/AikKCQrN0zqZFlg==
  • Date – 2010-09-16T07:50:45Z
  • Password – userpassword
  • Resulting Digest – tuOSpGlFlIXsozq4HFNeeGeFLEI=

后记

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


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



http://xzh.i3geek.com

爱唠叨的老鱼

爱唠叨的老鱼

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

0 条评论

发表评论

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

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