onvif协议控制之鉴权方式

onvif协议控制之鉴权方式

    • http鉴权方式
      • 认证协议与首部
      • http鉴权算法
        • http鉴权执行代码
        • http鉴权抓包数据如下
    • WS-UsernameToken令牌验证
        • 令牌验证执行代码
        • 令牌验证抓包数据如下

onvif协议内部应当支持两种鉴权方式,第一种名为http鉴权,第二种为WS-UsernameToken令牌验证。本文将着重介绍这两种鉴权方式

http鉴权方式

认证协议与首部

HTTP 请求通过增加特定头部信息,为不同的认证协议提供了一个可扩展框架。

步骤 首部 描述 方法状态
请求 第一条没有认证信息 GET
服务器返回 WWW-Authenticate 服务器用 401 状态拒绝了请求,说明需要用户提供用户名和密码鉴权 GET
再次请求鉴权 Authenticate 客户端重新发出请求,但这一次会附加一个Authorization首部,用来说明认证算法、用户名和密码 GET
服务器返回成功 服务器返回成功码200 GET

http鉴权算法

对用户名、认证域(realm)以及密码的合并值计算 MD5 哈希值,结果称为 HA1。
对HTTP方法以及URI的摘要的合并值计算 MD5 哈希值,例如,“GET” 和 “/dir/index.html”,结果称为 HA2。
对 HA1、服务器密码随机数(nonce)、请求计数(nc)、客户端密码随机数(cnonce)、保护质量(qop)以及 HA2 的合并值计算 MD5 哈希值。结果即为客户端提供的 response 值。
因为服务器拥有与客户端同样的信息,因此服务器可以进行同样的计算,以验证客户端提交的 response 值的正确性。在上面给出的例子中,结果是如下计算的。 (MD5()表示用于计算 MD5 哈希值的函数;“\”表示接下一行;引号并不参与计算)
根据上面的算法所给出的示例,将在每步得出如下结果。
HA1 = MD5(::
HA2 = MD5(:)
Response = MD5(HA1:::::HA2)

其中reaml、nonce、qop从服务器401时的头信息中获取
method:请求方式,如:“POST”、“GET”等
disgestUriPath:请求的uri ,如下图抓包中的 "/onvif/device_service"
conce:客户端生成的随机数
nc:请求次数
username:登录账号
psd:登录密码

http鉴权执行代码

普通的http请求,代码中使用到okhttp网络框架请求

    public static byte[] getByteArray2(String url, String user, String psd) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
//                .header("Authorization", "Client-ID " + UUID.randomUUID())
                .url(url)
                .get()
                .build();

        Response response = client.newCall(request).execute();
        if (response.isSuccessful() ) {
            if( response.code() == 200 &&  response.body() != null){
                return response.body().bytes();
            }
        } else if(response.code() == 401){ // 未鉴权去鉴权
            //WWW-Authenticate: Digest qop="auth", realm="DS-2CD2310FD-I", nonce="4d6a4931516a51304e554d364e445935595759785954553d", stale="TRUE"
            //WWW-Authenticate: Basic realm="DS-2CD2310FD-I"
            Headers h = response.headers();
            List<String> auths = h.values("WWW-Authenticate");
            Pattern qopPattern = Pattern.compile("qop=\"(.*?)\"");
            Pattern realmPattern = Pattern.compile("realm=\"(.*?)\"");
            Pattern noncePattern = Pattern.compile("nonce=\"(.*?)\"");
            String qop = "";
            String realm = "";
            String nonce = "";
            String method = request.method();
            String host = response.request().url().host();

            String disgestUriPath = url.split(host)[1];
            for (String head: auths) {
                Matcher qopMatcher = qopPattern.matcher(head);
                while (qopMatcher.find()){
                    try{
                        qop = qopMatcher.group(1);
                    } catch (Exception e){
                        LogClientUtils.d(tag, e.getMessage());
                    }
                }
                Matcher realmMatcher = realmPattern.matcher(head);
                while (realmMatcher.find()){
                    try{
                        realm = realmMatcher.group(1);
                    } catch (Exception e){
                        LogClientUtils.d(tag, e.getMessage());
                    }
                }
                Matcher nonceMatcher = noncePattern.matcher(head);
                while (nonceMatcher.find()){
                    try{
                        nonce = nonceMatcher.group(1);
                    } catch (Exception e){
                        LogClientUtils.d(tag, e.getMessage());
                    }
                }
            }
            return degistHttp(url, user, psd, method, disgestUriPath, nonce, realm, qop);
        }
        return null;
    }

增加鉴权信息请求

 private static byte[] degistHttp(String url, String user, String psd, String method, String disgestUriPath, String nonce, String realm, String qop) throws IOException {
        String nc = "00000001";
        String cnonce = getNonce();
        String ha1Data = getMd5Data(user, realm, psd);
        String ha1 = MD5Util.MD5Encode(ha1Data);
        String ha2Data = getMd5Data(method, disgestUriPath);
        String ha2 = MD5Util.MD5Encode(ha2Data);
        String ha3Data = getMd5Data(ha1, nonce, nc, cnonce, qop, ha2);
        String responseData = MD5Util.MD5Encode(ha3Data);
        String headFormat = "Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",nc=%s,response=\"%s\",qop=\"%s\"" ;
        String head = String.format(headFormat, user, realm, nonce, disgestUriPath, cnonce, nc, responseData, qop);
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
//                .header("Authorization", "Client-ID " + UUID.randomUUID())
                .url(url)
                .addHeader("Authorization", head)
                .get()
                .build();

        Response response = client.newCall(request).execute();
        if (response.isSuccessful() ) {
            if( response.code() == 200 &&  response.body() != null){
                return response.body().bytes();
            }
        }
        return null;
    }

    private static String getMd5Data(String... params){
        StringBuilder sb = new StringBuilder();
        for (String param : params){
            sb.append(param).append(":");
        }
        String data = sb.toString();
        return data.substring(0, data.length() - 1);
    }

    /**
     * 获取 Nonce
     *
     * @return Nonce
     */
    private static String getNonce() {
        //初始化随机数
        Random r = new Random();
        String text = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        String nonce = "";
        for (int i = 0; i < 32; i++) {
            int index = r.nextInt(text.length());
            nonce = nonce + text.charAt(index);
        }
        return nonce;
    }

http鉴权抓包数据如下

第一次不带鉴权数据请求修改onvif摄像头的密码:

hm¼\V~0Z:XEùEV¥@@À¨	À¨çPÀ8Úú¶D×PD
ßPOST /onvif/device_service HTTP/1.1
Host: 192.168.1.20
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 468

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <soap:Body>
    <tds:SetUser>
      <tds:User>
        <tt:Username>admin</tt:Username>
        <tt:Password>admin123</tt:Password>
        <tt:UserLevel>Administrator</tt:UserLevel>
      </tds:User>
    </tds:SetUser>
  </soap:Body>
</soap:Envelope>

服务器返回401

0Z:XEùhm¼\V~Eê @@ÊÚÀ¨À¨	Pçú¶D×À8ÜmPÛS/HTTP/1.1 401 Unauthorized
Date: Tue, 29 Oct 2019 10:54:50 GMT
Server: webserver
Content-Length: 218
Content-Type: text/html
Connection: close
WWW-Authenticate: Digest qop="auth", realm="DS-2CD2310FD-I", nonce="4e555130516a6b304e546f784e7a49334e7a417a59513d3d"

<!DOCTYPE html>
<html><head><title>Document Error: Unauthorized</title></head>
<body><h2>Access Error: 401 -- Unauthorized</h2>
<p>Authentication Error: Access Denied! Authorization required.</p>
</body>
</html>

增加鉴权数据再次请求:

hm¼\V~0Z:XEùEVª@@À¨	À¨çPkeqâhPDøPOST /onvif/device_service HTTP/1.1
Host: 192.168.1.20
Content-Type: application/soap+xml; charset=utf-8
Authorization: Digest username="admin", realm="DS-2CD2310FD-I", qop="auth", algorithm="MD5", uri="/onvif/device_service", nonce="4e555130516a6b304e546f784e7a49334e7a417a59513d3d", nc=00000001, cnonce="0EE3ED23BFD9A00B2AB542E3BAB85BDB", response="518fd6d1666f9f00a5c5097359188c4e"
Content-Length: 468

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <soap:Body>
    <tds:SetUser>
      <tds:User>
        <tt:Username>admin</tt:Username>
        <tt:Password>admin123</tt:Password>
        <tt:UserLevel>Administrator</tt:UserLevel>
      </tds:User>
    </tds:SetUser>
  </soap:Body>
</soap:Envelope>

服务器鉴权成功返回200

0Z:XEùhm¼\V~EíL$@@hyÀ¨À¨	PçqèkÕPôYÌ"http://docs.oasis-open.org/wsrf/bf-2" 
xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2" 
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" 
xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" 
xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics" 
xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl"><env:Body><tds:SetUserResponse/>
</env:Body>
</env:Envelope>

WS-UsernameToken令牌验证

从上面我们可以看到,倘若每一次post访问都需要鉴权的话,那么我们都需要进行两次网络访问。效率明显比较低,这个时候ONVIF的令牌验证方式,该方式可直接把鉴权信息写在请求体当中,一次网络访问即可解决战斗。算法如下
Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )

Nonce :随机数
Date:当前时间(格式: 2010-09-16T07:50:45Z)
Password :登录密码
Resulting Digest – tuOSpGlFlIXsozq4HFNeeGeFLEI=
生成 Digest

令牌验证执行代码

package com.wp.android_onvif.util;

import android.util.Base64;

import com.wp.android_onvif.onvifBean.Digest;

import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


public class Gsoap {

    /**
     * Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )
     * For example:
     *  Nonce – LKqI6G/AikKCQrN0zqZFlg==
     *  Date – 2010-09-16T07:50:45Z
     *  Password – userpassword
     *  Resulting Digest – tuOSpGlFlIXsozq4HFNeeGeFLEI=
     * 生成 Digest
     */
    public static Digest getDigest(String userName, String psw) {
        Digest digest = new Digest();
        String nonce = getNonce();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'",
                Locale.getDefault());
        String time = df.format(new Date());
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            // nonce需要用Base64解码一次
            byte[] b1 = Base64.decode(nonce.getBytes(), Base64.DEFAULT);
            // 生成字符字节流
            byte[] b2 = time.getBytes(); // "2018-01-10T11:00:00Z";
            byte[] b3 = psw.getBytes();
            // 根据我们传得值的长度生成流的长度
            byte[] b4;
            // 利用sha-1加密字符
            md.update(b1, 0, b1.length);
            md.update(b2, 0, b2.length);
            md.update(b3, 0, b3.length);
            // 生成sha-1加密后的流
            b4 = md.digest();
            // 生成最终的加密字符串
            String result = new String(Base64.encode(b4, Base64.DEFAULT)).trim();
//            Log.e("Gsoap", result);
            digest.setNonce(nonce);
            digest.setCreatedTime(time);
            digest.setUserName(userName);
            digest.setEncodePsw(result);
            return digest;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取 Nonce
     *
     * @return Nonce
     */
    private static String getNonce() {
        //初始化随机数
        Random r = new Random();
        String text = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        String nonce = "";
        for (int i = 0; i < 32; i++) {
            int index = r.nextInt(text.length());
            nonce = nonce + text.charAt(index);
        }
        return nonce;
    }

}

修改onvif摄像头密码请求数据体格式如下:
将我们登录账号、上面生成的加密密码EncodePsw、客户端生成的随机数和时间放入对应的位置当中去。需要注意在前后钱不要换行或者留有空格,否者服务端会将这个特殊字符当作参数进行计算,最后会与我们计算的值不一知道之验证失败

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope
    xmlns:s="http://www.w3.org/2003/05/soap-envelope">
    <s:Header>
        <Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <UsernameToken>
                <Username>%s</Username>
                <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</Password>
                <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</Nonce>
                <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</Created>
            </UsernameToken>
        </Security>
    </s:Header>
    <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <SetUser xmlns="http://www.onvif.org/ver10/device/wsdl">
            <User>
                <Username xmlns="http://www.onvif.org/ver10/schema">%s</Username>
                <Password xmlns="http://www.onvif.org/ver10/schema">%s</Password>
                <UserLevel xmlns="http://www.onvif.org/ver10/schema">Administrator</UserLevel>
            </User>
        </SetUser>
    </s:Body>
</s:Envelope>


向服务发送请求:
其中params就是上面的请求数据体

  /**
     * POST 请求
     */
    public static String postRequest(String baseUrl, String params) throws Exception {
        String receive = "";
        // 新建一个URL对象
        URL url = new URL(baseUrl);
        // 打开一个HttpURLConnection连接
        HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
        //设置请求允许输入 默认是true
        // Post请求必须设置允许输出 默认false
        urlConn.setDoInput(true);
        urlConn.setDoOutput(true);
        // 设置为Post请求
        urlConn.setRequestMethod("POST");
        // Post请求不能使用缓存
        urlConn.setUseCaches(false);
        //设置本次连接是否自动处理重定向
        urlConn.setInstanceFollowRedirects(true);
        // 配置请求Content-Type,application/soap+xml
        urlConn.setRequestProperty("Content-Type", "application/soap+xml");
        // 开始连接
        urlConn.connect();
        // 发送请求数据
        urlConn.getOutputStream().write(params.getBytes());
        // 判断请求是否成功
        if (urlConn.getResponseCode() == 200) {
            // 获取返回的数据
            InputStream is = urlConn.getInputStream();
            byte[] data = new byte[1024];
            int n;
            while ((n = is.read(data)) != -1) {
                receive = receive + new String(data, 0, n);
            }
        } else {
            throw new Exception("ResponseCodeError : " + urlConn.getResponseCode());
        }
        // 关闭连接
        urlConn.disconnect();
        return receive;
    }

令牌验证抓包数据如下

向服务器请求修改onvif摄像头密码

hm¼\V~0Z:XEùE@å@@À¨	À¨ïÆPCß<=iµáPDþ
<s:Envelope
    xmlns:s="http://www.w3.org/2003/05/soap-envelope">
    <s:Header>
        <Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <UsernameToken>
                <Username>admin</Username>
                <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">1zkPnFZ1FrpjoER17Y4j1IbvqaU=</Password>
                <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ySnRFg08OEejGk5jIz0WiacAAAAAAA==</Nonce>
                <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2019-10-29T03:24:35.938Z
                </Created>
            </UsernameToken>
        </Security>
    </s:Header>
    <s:Body
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <SetUser
            xmlns="http://www.onvif.org/ver10/device/wsdl">
            <User>
                <Username
                    xmlns="http://www.onvif.org/ver10/schema">admin
                </Username>
                <Password
                    xmlns="http://www.onvif.org/ver10/schema">admin123
                </Password>
                <UserLevel
                    xmlns="http://www.onvif.org/ver10/schema">Administrator
                </UserLevel>
            </User>
        </SetUser>
    </s:Body>
</s:Envelope>

服务器返回

0Z:XEùhm¼\V~EíÒ
@@âÀ¨À¨	PïÆi»Cß@³P ú	í"http://docs.oasis-open.org/wsrf/bf-2"
xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2"
xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2"
xmlns:trc="http://www.onvif.org/ver10/recording/wsdl"
xmlns:tse="http://www.onvif.org/ver10/search/wsdl"
xmlns:trp="http://www.onvif.org/ver10/replay/wsdl"
xmlns:tnshik="http://www.hikvision.com/2011/event/topics"
xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl"
xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema"
xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl">
<env:Body>
    <tds:SetUserResponse/>
</env:Body>undefined</env:Envelope>


// 我写的
<?xml version="1.0" encoding="utf-8"?>

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">  
  <s:Header> 
    <Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">  
      <UsernameToken> 
        <Username>admin</Username>  
        <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">CSguIFhe1wASn2Gol0HHhdOge34=</Password>  
        <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">YBsdsEpDReymoouS8qHAFJOAm1wa6hD3rFX6VYPk8SYSwZrukU8tbBI22E4gDu</Nonce>  
        <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2019-10-30T16:09:34.828Z</Created> 
      </UsernameToken> 
    </Security> 
  </s:Header>  
  <s:Body> 
    <tds:SetUser> 
      <tds:User> 
        <tt:Username>admin</tt:Username>  
        <tt:Password>admin123</tt:Password>  
        <tt:UserLevel>Administrator</tt:UserLevel> 
      </tds:User> 
    </tds:SetUser> 
  </s:Body> 
</s:Envelope>

你可能感兴趣的:(onvif,Android)