接到一个需求是开发下海康的球机,控制云台,给到我的是一个开发手册,当然了是海康的私有协议
ISAPI开发手册https://download.csdn.net/download/qq_37059136/88547425关于开发这块读文档就可以理解了,海康使用的是摘要认证,当然了海康已经给出使用范例
通过libcurl就可以直接连接上海康的球机了.那么是不是就这样了呢,很显然并不是,要是真的就用这种方法怎么能显示出在座的牛逼之处呢,有简单方法我不用,哎,就是玩
关于HTTP的认证方式有很多,应该是4种,basic 跟 Digest 最为常见,别的也不多说了,basic太简单了不提也罢,本文的关键是Digest (摘要)认证,至于摘要认证的发展跟原理这里不做赘述,请善用百度
关于Digest 认证,首先了解下这东西的收发流程:
1.你给服务器发送了一条命令 :get也好 put也好随便什么也好
2.服务器给你返回一个401 未认证错误 以及附带给你将要在摘要认证中需要的参数
3.你拿到服务器返回的参数,根据摘要认证规则算出认证响应值在此发送给服务器
rfc7616 规则https://datatracker.ietf.org/doc/html/rfc7616#section-3.3
4.服务器响应你的操作并返回正确
步骤可参考下图
那么知道流程了,具体该怎么操作呢,我知道你很急但是你先别急,先来看看这四个流程具体发送了社么报文(通过wireshark抓包)
1.首先客户端发送了一条get指令获取
2.客户端返回了一个401错误
这张图的知识点比较多了,主要看下面两个
WWW-Authenticate: Digest qop="auth", realm="IP Camera(AE424)", nonce="353334613a31313564353336613a06002d080ad3bdce5c80d6282269c72b", stale="FALSE"\r\n
<!DOCTYPE html>\r\n
<html><head><title>Document Error: Unauthorized</title></head>\r\n
<body><h2>Access Error: 401 -- Unauthorized</h2>\r\n
<p>Authentication Error</p>\r\n
</body>\r\n
</html>
首先下面的html告诉你本次get失败了,错误原因是Unauthorized(未经授权)
然后再看上面WWW-Authenticate,这段信息量很大
Digest 认证方式:摘要认证
qop="auth" 保护质量 一般为auth/auth-int或缺失
realm="IP Camera(AE424)" 领域
nonce 服务器随机数
stale 是否过期(上述摘要认证参数是否过期)
这几个参数可以在上面的rfc7616 规则里面通过ctrl+f 查找,看看具体怎么定义这几个参数的,下文中还有几个参数也需要在rfc7616规则中查找
3.计算出认证响应值后再次发送
可以看到本次发送的消息头里面添加了上次服务器返回的摘要认证参数,以及最重要的response参数
4.认证成功,服务器应答
好了上面的步骤跟报文都分析结束了,应该可以看出来,第三步发送的参数才是真正的关键点
下面开始讲下如何计算response 参数
首先给出三个公式
那么先来解答几个关键问题
- 问:公式里面有算法MD5和MD5-sess,那么摘要认证应该用什么算法呢?
答:缺省状态下是MD5,而且现在只支持MD5
2.问:怎么理解上述图表?
答:上述图表给出三个东西,分别是 A1,A2,摘要计算(response 参数)
我们要求摘要计算(response 参数),那么得出公式
response=MD5(<A1>:<nonce>:<nc>:<cnonce>:<qop>:<A2>)
已知公式response,还需要知道A1跟A2
再根据图表
A1=MD5(<user>:<realm>:<password>)
A2=MD5(<request-method>:<uri-directive-value>)
那么很显然,只要我们搞定
<user> 用户名
<realm> 领域
<password> 密码
<request-method> 请求方式(get/put/pust等之一)
<uri-directive-value> uri跳转地址: http://192.168.1.64:80/ISAPI/PTZCtrl/channels/1/continuous 中/ISAPI/PTZCtrl/channels/1/continuous部分就是这里的<uri-directive-value>
<nonce> 服务器随机数
<nc> 客户端计数器 一般为1 十六进制00000001
<cnonce> 客户端随机数
<qop> 保护质量 一般为auth/auth-int或缺失
这几个参数就可以了,当然了还需搞定MD5算法(上述几个参数务必在rfc7616 规则里自行查看含义)
这几个参数中尚未可知的只有 <nc> <cnonce>
nc 一般就是1 ,cnonce 是随机数,需要客户端生成的
这块的计算代码如下
cpp
//计算摘要
QString CHTTP::GetDigestResponse()
{
//计算A1 A1=MD5(<user>:<realm>:<password>)
QString StrA1 = QString("%1:%2:%3").arg(m_AuthInfo_.user).arg(m_AuthInfo_.realm).arg(m_AuthInfo_.passward);
QString MD5_A1 = MD5_hash32(*this,StrA1);
qDebug() << MD5_A1;
//计算A2 A2=MD5(<request-method>:<uri-directive-value>)
QString StrA2 = QString("%1:%2").arg(m_AuthInfo_.requestMethod()).arg(m_AuthInfo_.uri);
QString MD5_A2 = MD5_hash32(*this,StrA2);
qDebug() << MD5_A2;
//计算response response=MD5(<A1>:<nonce>:<nc>:<cnonce>:<qop>:<A2>)
char hex[32] = {0};
snprintf(hex,sizeof(hex),"%08x",m_AuthInfo_.nc);
QString response = QString("%1:%2:%3:%4:%5:%6").arg(MD5_A1).arg(m_AuthInfo_.nonce).arg(hex).arg(m_AuthInfo_.cnonce).arg(m_AuthInfo_.qop).arg(MD5_A2);
QString MD5_response = MD5_hash32(*this,response);
qDebug() << MD5_response;
return MD5_response;
}
现在有了response ,就可以去发报文了
报文发送如下:
cpp
void CHTTP::IdentityAuthentication() //身份认证
{
// 设置请求的网址 //认证
m_req->setUrl(m_url_);
// 设置请求头的Content-Type (以xml类型为例)
m_req->setHeader(QNetworkRequest::ContentTypeHeader,QVariant("application/xml;charset=utf-8"));
//计算摘要
QString response = GetDigestResponse();
//设置通讯头
char hex[32] = {0};
snprintf(hex,sizeof(hex),"%08x",m_AuthInfo_.nc);
QString headstr = QString("username=\"%1\", realm=\"%2\", nonce=\"%3\", uri=\"%4\",response=\"%5\", qop=%6, nc=%7, cnonce=\"%8\"")
.arg(m_AuthInfo_.user).arg(m_AuthInfo_.realm).arg(m_AuthInfo_.nonce).arg(m_AuthInfo_.uri)
.arg(response).arg(m_AuthInfo_.qop).arg(hex).arg(m_AuthInfo_.cnonce);
m_req->setRawHeader("Authorization", "Digest " + headstr.toLatin1());
// 设置请求体 并 发送请求
if(m_AuthInfo_.reqType == request_method::GET)
m_reply = m_manager->get(*m_req);
else if(m_AuthInfo_.reqType == request_method::PUT)
{
if(m_ptztype_ == ptztype::Up)
{
m_reply = m_manager->put(*m_req,ptzUp_.toStdString().c_str());
}
else if(m_ptztype_ == ptztype::Down)
{
m_reply = m_manager->put(*m_req,ptzDown_.toStdString().c_str());
}
else if(m_ptztype_ == ptztype::Stop)
{
m_reply = m_manager->put(*m_req,ptzStop_.toStdString().c_str());
}
}
// 开启事件监听,直到完成响应
QEventLoop loop;
connect(m_reply, &QNetworkReply::finished, &loop,&QEventLoop::quit);
loop.exec();
}