公司接了一个项目需要对接海康的一些硬件设备,第一次对接硬件,记录一下海康ISAPI接口的访问调用过程...
项目主要功能就是食堂的报餐记录下发、考勤的人员下发及记录获取。功能并不复杂,主要是对硬件对接不熟悉,网上大多都是海康视频监控类的文章,没有找到其他类型的设备对接的文章,所以过程也是比较煎熬的。业务场景就不作叙述了,文章主要记录一下海康ISAPI接口的调用访问。
准备
- 根据海康硬件设备的型号登录海康开放平台的官网: open.hikvision.com ; 在个人中心的通用协议中获取对应设备的ISAPI协议开发文档。
- 获取到对应设备的ISAPI文档后按照需求进行接口的查找实现功能就行了。
注意事项
- 不是所有的设备都支持ISAPI协议开发,需要与海康的技术人员做好对接。(我这边的考勤机和消费机都支持)
- 查看文档后发现,海康的ISAPI接口访问需要进行摘要认证才行。
DIGEST 认证的步骤
步骤 1: 请求需认证的资源时,服务器会随着状态码 401Authorization Required,返回带WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需的临时质询码(随机数,nonce)。首部字段 WWW-Authenticate 内必须包含realm 和nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。nonce 是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由Base64 编码的十六进制数的组成形式,但实际内容依赖服务器的具体实现。
步骤 2:接收到401状态码的客户端,返回的响应中包含 DIGEST 认证必须的首部字段 Authorization 信息。首部字段 Authorization 内必须包含 username、realm、nonce、uri 和response的字段信息。其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。
username是realm 限定范围内可进行认证的用户名。uri(digest-uri)即Request-URI的值,但考虑到经代理转发后Request-URI的值可能被修改因此事先会复制一份副本保存在 uri内。
response 也可叫做 Request-Digest,存放经过 MD5 运算后的密码字符串,形成响应码。
步骤 3:接收到包含首部字段 Authorization 请求的服务器,会确认认证信息的正确性。认证通过后则返回包含 Request-URI 资源的响应。并且这时会在首部字段 Authentication-Info 写入一些认证成功的相关信息。(不过我下面的例子没有去写这个Authentication-Info,而是直接返回的数据。因为我实在session里缓存的认证结果)。
Digest 认证涉及到的参数的含义
WWW-Authentication:用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源
realm:表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
qop:保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
nonce:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击
nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
cnonce:客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令
Authorization-Info:用于返回一些与授权会话相关的附加信息
nextnonce:下一个服务端随机数,使客户端可以预先发送正确的摘要
rspauth:响应摘要,用于客户端对服务端进行认证
stale:当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了
摘要认证的实现
简易写法
因为主要逻辑和接口都是是前端写,针对在浏览器环境下发现一个简洁的绕过编写摘要认证即可访问海康ISAPI接口的方式(谷歌能用):
//例如: 使用axios库
// 接口:ISAPI/**/**/**?format=json
// 请求方式: PUT
// 请求域名:协议 + 设备本地IP : http://127.0.0.1/
// 设备后台的账号密码: user,password (text = user + ":" + password + "@@")
axios.put("http://" + text + "127.0.0.1/ISAPI/**/**/**?format=json")
代码编写摘要认证
使用的axios库
摘要使用的是MD5加密
username: 设备的后台登录的账号
password: 设备的后台登录的密码
axios.post(协议+设备IP+接口, 传参).then(res=>{
//访问成功
}).catch(error=>{
//访问报错 第一次访问没有传入摘要信息,返回401 响应头中会有带 `WWW-Authentication` 字段的响应信息返回
//该字段内包含质问响应方式认证所需的临时质询码(随机数,nonce)。首部字段 WWW-Authenticate 内必须包含realm 和nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的
if(error.response.status == 401){
const cnonce = md5(String(new Date().getTime()));
const auth = error.response.headers['www-authenticate'];
let realm, nonce, qop;
const authSplit = auth.split(',');
for (const k in authSplit) {
if (authSplit[k].indexOf('realm=') >= 0) {
const realmSplit = authSplit[k].split('="');
realm = realmSplit[realmSplit.length - 1];
realm = realm.substring(0, realm.length - 1);
}
if (authSplit[k].indexOf('nonce=') >= 0) {
const nonceSplit = authSplit[k].split('nonce="');
nonce = nonceSplit[nonceSplit.length - 1];
nonce = nonce.substring(0, nonce.length - 1);
}
if (authSplit[k].indexOf('qop=') >= 0) {
const qopSplit = authSplit[k].split('="');
qop = qopSplit[qopSplit.length - 1];
qop = qop.substring(0, qop.length - 1);
}
}
const HA1 = md5(username + ':' + realm + ':' + password);
const HA2 = md5(`请求方式` + ':' + `接口路径`);
const response = md5(HA1 + ':' + nonce + ':00000001:' + cnonce + ':' + qop + ':' + HA2);
const Authorization =
'Digest username="' + `账号` +
'",realm="' + realm +
'",nonce="' + nonce +
'",uri="' + `接口路径` +
'",cnonce="' + cnonce +
'",nc=00000001,algorithm=MD5,response="' + response +
'",qop="' + qop + '"';
//设置请求头的 `Authorization` 重新请求
axios.post(协议+设备IP+接口, 传参,{
headers: {
'Authorization': Authorization
}
}).then(res=>{}).catch();
}
});
这样就能实现海康ISAPI接口的访问调用,可根据业务自己封装。如果出现跨域问题,配置一个nginx代理即可。
//nginx.conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#连接超时时间,服务器会在这个时间过后关闭连接。
keepalive_timeout 10;
# gizp压缩
gzip on;
client_max_body_size 20M;
# 直接请求nginx也是会报跨域错误的这里设置允许跨域
# 如果代理地址已经允许跨域则不需要这些, 否则报错(虽然这样nginx跨域就没意义了)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
#??????项目访问??????
server {
listen 80;
server_name `项目地址`;
location / {
# 配置代理地址
proxy_set_header X-Forward-For $remote_addr;
proxy_set_header X-real-ip $remote_addr;
proxy_pass `设备IP地址`;
proxy_set_header Host $host:$server_port;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
}
}