文章目录
- 1.前言
- 2. 原理
- 3. 过程
- 4. node实现摘要认证
- 5. 前端如何Digest摘要登录认证(下面是海康的设备代码)
1.前言
根据项目需求,海康设备ISAPI协议需要摘要认证,那么什么是摘要认证?估计不少搞到几年的前端连摘要认证都不知道是什么?能解决什么问题?我们平时用的比较多的是Basic基础认证,Digest 摘要认证比 Basic 基础认证的安全级别更高,它可以通过传递用户名、密码等计算出来的摘要来解决明文方式在网络上发送密码的问题,通过服务产生随机数 nonce 的方式可以防止恶意用户捕获并重放认证的握手过程。
2. 原理
1.客户端发出一个没有认证证书的请求,下面示例为用户名密码校验的ISAPI协议命令(GET方法),每次下发新的命令都需要重新认证。
GET /ISAPI/Security/userCheck HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: 10.18.37.12
Connection: Keep-Alive
2. 服务器产生一个随机数nonce,并且将该随机数放在WWW-Authenticate响应头,与服务器支持的认 证算法列表,认证的域realm一起发送给客户端,401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式,stale表示nonce值是否过期,如果过期会生成新的随机数。
HTTP/1.1 401 Unauthorized
Date: Wed, 01 February 2023 19:16:52 GMT
Server: App-webs/
Content-Length: 178
Content-Type: text/html
Connection: keep-alive
Keep-Alive: timeout=10, max=99
WWW-Authenticate: Digest qop="auth", realm="IP Camera(12345)",
nonce="4e5749344e7a4d794e544936596a4933596a51784e44553d", stale="FALSE"
看到上面出现了那么多之前没见过的参数,下面会做出详细解释:
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,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了
3. 客户端接收到401响应表示需要进行认证,选择一个算法(目前只支持MD5)生成一个消息摘要(message digest,该摘要包含用户名、密码、给定的nonce值、HTTP方法以及所请求的URL),将摘要放到Authorization的请求头中重新发送命令给服务器。如果客户端要对服务器也进行认证,可以同时发送客户端随机数cnonce,客户端是否需要认证,通过报文里面的qop值进行判断。
GET /ISAPI/Security/userCheck HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: zh-CN
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: 10.18.37.12
Connection: Keep-Alive
Authorization: Digest username="admin",realm="IP
Camera(12345)",nonce="595463314d5755354d7a4936596a49344f475a6a5a44453d",uri="/ISAPI/Security/userCheck",cnonc
e="011e08f6c9d5b3e13acfa810ede73ecc",nc=00000001,response="82091ef5aaf9b54118b4887f8720ae06",qop="auth"
4. 服务接收到摘要,选择算法以及掌握的数据,重新计算新的摘要跟客户端传输的摘要进行比较,验证是否匹配,若客户端反过来用客户端随机数对服务器进行质询,就会创建客户端摘要,服务可以预先将下一个随机数计算出来,提前传递给客户端,通过 Authentication-Info 发送下一个随机数。该步骤选择实现。
HTTP/1.1 200 OK
Date: Wed, 01 February 2023 19:16:52 GMT
Server: App-webs/
Content-Length: 132
Connection: keep-alive
Keep-Alive: timeout=10, max=98
Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<userCheck>
<statusValue>200</statusValue>
<statusString>OK</statusString>
</userCheck>
3. 过程
在说明如何计算摘要之前,先说明参加摘要计算的信息块。信息块主要有两种:
表示与安全相关的数据的A1,A1中的数据时密码和受保护信息的产物,它包括用户名、密码、保护域和随机数等内容,A1只涉及安全信息,与底层报文自身无关.
若算法是:MD5 A1 = < user >:< realm >:< password>
若算法是:MD5-sess
则 A1=MD5( < user > :< realm >:< password >):< nonce >:< cnonce >
表示与报文相关的数据的A2,A2表示是与报文自身相关的信息,比如URL,请求反复和报文实体的主体部分,A2加入摘要计算主要目的是有助于防止反复,资源或者报文被篡改。
若 qop 未定义或者 auth:
A2=< request-method >:< uri-directive-value >
若 qop 为 auth:-int
A2=< request-method >:< uri-directive-value >:MD5(< request-entity-body >)
注:< uri-directive-value >为完整的协议命令 URI,比如“/ISAPI/ContentMgmt/InputProxy/channels/status”。
最重要的一步,定义摘要的计算规则
若 qop 没有定义:
摘要 response=MD5(MD5(A1):< nonce >:MD5(A2))
若 qop 为 auth:
摘要 response=MD5(MD5(A1):< nonce >:< nc >:< cnonce >:< qop >:MD5(A2))
若 qop 为 auth-int:
摘要 response= MD5(MD5(A1):< nonce >:< nc >:< cnonce >:< qop >:MD5(A2))
4. node实现摘要认证
const http = require('http');
const crypto = require('crypto');
const realm = '1e7d1893b17de03ccc122a4f';
const users = {
'admin': 'Coolyuan5g',
'admin1': 'kg5g',
};
const authenticate = (res) => {
res.setHeader('WWW-Authenticate',
`Digest realm="${realm}", nonce="${Date.now()}", algorithm=MD5, qop="auth"`);
res.statusCode = 401;
res.end('Unauthorized');
};
const validate = (auth, password) => {
const hash = crypto.createHash('md5');
hash.update(`${auth.username}:${realm}:${password}`);
return hash.digest('hex') === auth.response;
};
const parseAuth = (auth) => {
const [, parameters] = auth.split(' ');
const params = {};
parameters.split(', ').forEach((param) => {
const [key, value] = param.split('=');
params[key] = value.replace(/\"/g, '');
});
return params;
};
const server = http.createServer((req, res) => {
if (!req.headers.authorization) {
return authenticate(res);
}
const auth = parseAuth(req.headers.authorization);
if (!auth || !auth.username || !auth.realm || !auth.nonce || !auth.uri || !auth.response || auth
.realm !== realm) {
return authenticate(res);
}
if (!users[auth.username] || !validate(auth, users[auth.username])) {
return authenticate(res);
}
res.end('Authorized');
});
5. 前端如何Digest摘要登录认证(下面是海康的设备代码)
const axios = require('axios'),
md5 = require('md5-node');
function generateCnonce() {
let cnonce = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
cnonce += possible.charAt(Math.floor(Math.random() * possible.length));
}
return cnonce;
}
//获取超脑设备所有的设备通道信息
router.get('/getChannels', function (req, res, next) {
const params = req.query;
if (params.ip && params.userName && params.password) {
axios.get(
`http://${params.ip}/ISAPI/ContentMgmt/InputProxy/channels/status`
).then((response) => {
console.log(response)
}).catch((error) => {
//第一次请求401,获取服务请求头返回的数据
// 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, 表示服务器在告知客户端用新的随机数来重试, 而不再要求用户重新输入用户名和密码了
// Digest realm="1e7d1893b17de03ccc122a4f", domain="/", qop="auth", nonce="d4cdb906ef173773:1e7d1893b17de03ccc122a4f:1863093aa1e:e", opaque="799d5", algorithm="MD5", stale="FALSE"
if (error.response.status == 401) {
let info = error.response.headers["www-authenticate"],
cnonce = generateCnonce(),
nonce = info.match(/\snonce="([^"]+)/)[1],
opaque = info.match(/\sopaque="([^"]+)/)[1],
qop = info.match(/\sqop="([^"]+)/)[1],
realm = info.match(/\srealm="([^"]+)/)[1],
uri = `/ISAPI/ContentMgmt/InputProxy/channels/status`,
a1Md5 = md5(`admin:${realm}:Coolyuan`),
a2Md5 = md5(`GET:${uri}`),
//这里的response计算是关键!!!通过md5计算拼接好的字符串
response = md5(`${a1Md5 }:${nonce}:${nc}:${cnonce}:auth:${a2Md5 }`),
authorization= `Digest username="admin", realm="${realm}", nonce="${nonce}", uri=${uri}, algorithm=MD5, response="${response}", opaque="${opaque}", qop=${qop}, nc=00000001, cnonce="${cnonce}"`
//实现第二次请求
axios.get(
`http://${params.ip}/ISAPI/ContentMgmt/InputProxy/channels/status`, {
headers: {
'Authorization': authorization
}
}
).then((data) => {
res.status(200).send({
"code": 200,
"message": "成功",
"result": "success",
"content": data.data
});
}).catch((error) => {
res.status(500).send({
"code": 500,
"message": "失败",
"result": "fail",
"content": null
});
})
}
})
} else {
res.status(401).send({
"message": "失败",
"result": "fail",
"content": null
});
}
});
module.exports = router;
以上部分是代码的核心部分,仅供参考!谢谢!