Skip to content

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 包含:

参数值说明
realmmingshang固定值
qopauth质量保护
nonce服务端生成的随机值一次性消费
opaquems-digest-opaque原样返回
algorithmMD5固定值

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 示例 ​

参数值
serialESP32-001
realmmingshang
authCodea1b2c3d4e5f6...
methodPOST
uri/api/device/http/property
noncecf8a3f177a1b
nc00000001
cnoncedevice-random-xyz
qopauth
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 登录态。