HTTP Digest 认证设备接入
名尚智联支持 RFC 7616 HTTP Digest Access Authentication,为不支持 MQTT/CoAP 的 HTTP 设备提供接入通道。端点 /api/device/http/** 已配置为 Sa-Token 白名单,认证由 DigestAuthenticator 自验。
一、接入流程
1. 设备 POST /api/device/http/property
→ 无认证 → 401 + WWW-Authenticate: Digest ...
2. 设备解析 challenge,计算 response
HA1 = MD5(serial:realm:authCode)
HA2 = MD5(POST:/api/device/http/property)
response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
3. 设备带 Authorization 头重发 POST
→ 服务端 nonce 一次性消费防重放
→ response 校验 → 成功上报二、接口定义
POST /api/device/http/property
上报设备属性。
Request:
POST /api/device/http/property HTTP/1.1
Host: your-server.com
Authorization: Digest username="ESP32-001",
realm="mingshang",
nonce="abc123...",
uri="/api/device/http/property",
qop=auth,
nc=00000001,
cnonce="xyz789",
response="d41d8cd98f00b204e9800998ecf8427e",
opaque="ms-digest-opaque"
Content-Type: application/json
Content-Length: 62
{"props":{"temperature":25.6,"humidity":62.3}}Response 200:
json
{"code": 0, "msg": "ok"}Response 401(首次请求):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="mingshang",
qop="auth",
nonce="cf8a3f177a1b...",
opaque="ms-digest-opaque",
algorithm=MD5
{"code": 401, "msg": "Unauthorized"}三、Digest 计算详解
3.1 挑战参数
服务端返回的 WWW-Authenticate 包含:
| 参数 | 值 | 说明 |
|---|---|---|
realm | mingshang | 固定值 |
qop | auth | 质量保护 |
nonce | 服务端生成的随机值 | 一次性消费 |
opaque | ms-digest-opaque | 原样返回 |
algorithm | MD5 | 固定值 |
3.2 设备侧计算
python
import hashlib
def md5hex(s: str) -> str:
return hashlib.md5(s.encode('utf-8')).hexdigest()
# 1. HA1
HA1 = md5hex(f"{serial}:{realm}:{authCode}")
# 2. HA2
HA2 = md5hex(f"{method}:{uri}")
# 3. response
response = md5hex(f"{HA1}:{nonce}:{nc}:{cnonce}:{qop}:{HA2}")3.3 示例
| 参数 | 值 |
|---|---|
| serial | ESP32-001 |
| realm | mingshang |
| authCode | a1b2c3d4e5f6... |
| method | POST |
| uri | /api/device/http/property |
| nonce | cf8a3f177a1b |
| nc | 00000001 |
| cnonce | device-random-xyz |
| qop | auth |
HA1 = MD5("ESP32-001:mingshang:a1b2c3d4e5f6...")
HA2 = MD5("POST:/api/device/http/property")
response = MD5(HA1:nonce:nc:cnonce:qop:HA2)四、Python 完整示例
python
import hashlib
import time
import urllib.request
import urllib.error
import re
SERIAL = "ESP32-001"
AUTHCODE = "a1b2c3d4e5f6..."
URL = "https://your-server.com/api/device/http/property"
REALM = "mingshang"
OPAQUE = "ms-digest-opaque"
def md5(s): return hashlib.md5(s.encode()).hexdigest()
def http_digest_post(url, serial, auth_code, payload):
# 1. 首次请求拿 challenge
try:
req = urllib.request.Request(url, data=payload.encode(), method="POST")
urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
if e.code != 401: raise
auth_header = e.headers["WWW-Authenticate"]
# 2. 解析 challenge 参数
params = dict(re.findall(r'(\w+)="([^"]*)"', auth_header))
params.update(re.findall(r'(\w+)=([^,\s]+)', auth_header)) # 非引号值
nonce = params["nonce"]
uri = re.search(r'uri="([^"]*)"', auth_header).group(1) if 'uri=' in auth_header else url.replace("https://", "").split("/")[0] + url
# 3. 计算 Digest
cnonce = md5(str(time.time()))[:16]
nc = "00000001"
HA1 = md5(f"{serial}:{REALM}:{auth_code}")
HA2 = md5(f"POST:{uri}")
resp_value = md5(f"{HA1}:{nonce}:{nc}:{cnonce}:auth:{HA2}")
# 4. 带 Authorization 重发
auth = f'Digest username="{serial}", realm="{REALM}", nonce="{nonce}", uri="{uri}", ' \
f'qop=auth, nc={nc}, cnonce="{cnonce}", response="{resp_value}", opaque="{OPAQUE}"'
req2 = urllib.request.Request(url, data=payload.encode(), method="POST")
req2.add_header("Authorization", auth)
req2.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req2) as r:
return r.read().decode()
# 使用
payload = '{"props":{"temperature":25.6,"humidity":62.3}}'
result = http_digest_post(URL, SERIAL, AUTHCODE, payload)
print(result) # {"code":0,"msg":"ok"}五、curl 示例
bash
# 首次请求(拿 challenge)
curl -v -X POST https://your-server.com/api/device/http/property \
-H "Content-Type: application/json" \
-d '{"props":{"temperature":25.6}}'
# → 401 + WWW-Authenticate
# 用 curl --digest 自动处理(需用户名=serial, 密码=authCode)
curl -X POST https://your-server.com/api/device/http/property \
--digest -u "ESP32-001:a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{"props":{"temperature":25.6}}'
# → {"code":0,"msg":"ok"}六、安全说明
| 机制 | 说明 |
|---|---|
| 防重放 | nonce 一次性消费,TTL 5 分钟 |
| 常时比较 | response 用 MessageDigest.isEqual 防止时序攻击 |
| HTTPS | 生产环境强制 HTTPS(Cloudflare/Ingress 终止 TLS) |
| authCode | 设备 authCode 明文存储,Digest 密码即 authCode |
七、Sa-Token 白名单
/api/device/http/** 已在 SaTokenConfig 中配置为 excludePathPatterns,认证由 DigestAuthenticator 自验,不走 Sa-Token 登录态。