HTTP Digest 认证:原理剖析与服务端实现详解
HTTP Digest 认证:原理剖析与服务端实现详解
HTTP 协议中的 Digest 认证(摘要认证)是一种比 Basic 认证更安全的身份验证机制,其核心设计是避免密码明文传输,并通过动态随机数(Nonce)防范重放攻击。本文将结合标准协议流程与具体服务端代码实现,深入解析 Digest 认证的技术细节。
一、Digest 认证的核心原理与流程
1.1 为什么需要 Digest 认证?
Basic 认证直接通过 Base64 编码传输用户名和密码(如 username:password
),虽然编码可逆但无加密,安全性极差。Digest 认证通过哈希算法(如 MD5)对密码进行处理,客户端仅传输密码的哈希值(而非明文),且每次请求使用动态随机数(Nonce),大幅提升了安全性。
1.2 标准协议流程(RFC 7616)
Digest 认证的核心是 “挑战 - 响应” 模式,完整流程可分为 4 步:
步骤 1:客户端首次请求受保护资源
客户端访问服务端的受保护路径(如示例中的 /sd
),未携带认证信息。
步骤 2:服务端返回 401 挑战(Challenge)
服务端返回状态码 401 Unauthorized
,并在 WWW-Authenticate
头部中携带以下关键参数:
realm
:认证域(如MyProtectedSD
),用于提示用户认证的上下文(如 “我的受保护存储”)。nonce
:服务端生成的一次性随机数(如a1b2c3...
),用于防止重放攻击。opaque
:服务端生成的固定字符串(如 MD5 哈希值),客户端需原样返回。qop
:认证质量(Quality of Protection),常见值为auth
(仅验证请求)。algorithm
:哈希算法(默认MD5
)。
示例头部:
WWW-Authenticate: Digest realm="MyProtectedSD", qop="auth", nonce="a1b2c3...", opaque="d4e5f6...", algorithm="MD5"
步骤 3:客户端构造认证响应(Response)
客户端收到挑战后,提示用户输入用户名和密码,计算并组装 Authorization
头部,包含以下参数:
-
username
:用户输入的用户名。 -
realm
:服务端返回的realm
(需与本地存储的密码关联)。 -
nonce
:服务端返回的nonce
(原样使用)。 -
uri
:请求的资源路径(如/sd
)。 -
response:核心哈希值,通过以下公式计算:
response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
其中:
HA1 = MD5(username:realm:password)
(客户端预计算的用户密码哈希)。HA2 = MD5(method:uri)
(请求方法与资源路径的哈希,如GET:/sd
)。nc
:请求计数(Nonce Count,防止重放攻击的递增序号)。cnonce
:客户端生成的随机数(Client Nonce,增强随机性)。
步骤 4:服务端验证响应
服务端收到 Authorization
头部后,根据以下逻辑验证:
- 校验
realm
、nonce
(是否有效且未过期)、opaque
是否匹配。 - 根据用户名获取存储的密码(或预计算的
HA1
)。 - 重新计算
HA1
、HA2
和期望的response
。 - 比较客户端提供的
response
与服务端计算的response
,一致则认证成功。
二、服务端实现逻辑:代码解析
本文提供的服务端代码基于 Python 的 http.server
模块,实现了 Digest 认证的核心逻辑。以下是关键模块的详细解析。
2.1 全局配置与状态管理
# --- 配置 ---
SERVER_ADDRESS = '0.0.0.0' # 监听所有接口,方便测试
SERVER_PORT = 8001
REALM = "MyProtectedSD" # 保护域名,会显示在客户端的认证提示中# 存储用户名和密码(在实际应用中,密码应该哈希存储,这里为了演示方便直接存储)
# 或者更好的方式是存储 HA1 = MD5(username:realm:password)
# 例如: HA1 = hashlib.md5(f"your_username_here:{REALM}:your_password_here".encode()).hexdigest()
USERS_PASSWORDS = {"admin001": "my_password_random_generation"
}# 存储已发出的 nonce 及其创建时间,用于校验和防止重放
# {nonce_value: creation_timestamp}
# 实际应用中,nonce 应该有过期机制,并且用后即焚或严格校验 nc (nonce count)
active_nonces = {}
NONCE_LIFETIME_SECONDS = 300 # Nonce 有效期,例如 5 分钟# Opaque 值,服务器生成,客户端原样返回
OPAQUE = hashlib.md5(os.urandom(16)).hexdigest()
- 用户存储:示例中直接存储明文密码(实际生产环境应存储预计算的
HA1 = MD5(username:realm:password)
)。 - nonce 管理:
active_nonces
字典记录nonce
及其生成时间,用于后续过期校验。 - opaque:服务端生成的固定字符串,防止客户端篡改挑战参数。
2.2 nonce 生成与过期校验
def generate_nonce():"""生成唯一的 nonce 并记录创建时间"""nonce = hashlib.md5((os.urandom(16).hex() + str(time.time())).encode()).hexdigest()active_nonces[nonce] = time.time() # 存储 nonce 与时间戳return nonce
nonce
是 Digest 认证的安全基石,其生成需满足:
- 随机性:通过
os.urandom(16)
生成随机字节,结合时间戳确保唯一性。 - 时效性:
active_nonces
记录生成时间,超过NONCE_LIFETIME_SECONDS
(5 分钟)后失效,防止重放攻击。
2.3 挑战响应(401 状态码)
def send_401_challenge(self, stale=False):"""发送 401 Unauthorized 响应和 WWW-Authenticate 挑战头"""self.send_response(401)self.send_header('Content-type', 'text/plain')nonce = generate_nonce() # 生成新 nonceauth_challenge = f'Digest realm="{REALM}", qop="auth", nonce="{nonce}", opaque="{OPAQUE}", algorithm="MD5"'if stale:auth_challenge += ', stale="true"' # 标记旧 nonce 过期,提示客户端使用新 nonceself.send_header('WWW-Authenticate', auth_challenge)self.end_headers()self.wfile.write(b"Authentication required.")print(f"Sent 401 challenge with new nonce: {nonce}")
当客户端未携带认证信息或认证失败时,服务端返回 401
状态码,并通过 WWW-Authenticate
头部发送挑战参数。若 stale=True
(如 nonce
过期),客户端会自动使用新 nonce
重试。
2.4 认证头解析与验证
2.4.1 解析 Authorization 头部
def parse_digest_auth_header(auth_header_value):"""解析 Authorization: Digest ... 头部字符串返回一个包含各参数的字典"""if not auth_header_value or not auth_header_value.lower().startswith('digest '):return Noneauth_parts = {}# 移除 "Digest " 前缀value_str = auth_header_value[len('Digest '):]# 使用正则表达式解析 key="value" 或 key=value 对# 这个正则表达式处理了带引号和不带引号的值pattern = re.compile(r'(\w+)=(?:"([^"]*)"|([^\s,]*))')for match in pattern.finditer(value_str):key = match.group(1)# 值可能在 group(2) (带引号) 或 group(3) (不带引号)val = match.group(2) if match.group(2) is not None else match.group(3)auth_parts[key] = valreturn auth_parts
客户端发送的 Authorization
头部是一个复杂的字符串(如 Digest username="admin001", realm="MyProtectedSD", nonce="a1b2c3", ...
),此函数通过正则表达式提取各参数,供后续验证使用。
2.4.2 验证认证响应
def verify_digest_response(self, auth_parts):"""校验客户端提供的 Digest 认证信息"""required_keys = ['username', 'realm', 'nonce', 'uri', 'response', 'qop', 'nc', 'cnonce']for key in required_keys:if key not in auth_parts:print(f"Missing digest auth key: {key}")return Falseusername = auth_parts.get('username')client_realm = auth_parts.get('realm')client_nonce = auth_parts.get('nonce')uri = auth_parts.get('uri')client_response = auth_parts.get('response')qop = auth_parts.get('qop')nc = auth_parts.get('nc') # nonce countcnonce = auth_parts.get('cnonce') # client noncealgorithm = auth_parts.get('algorithm', 'MD5').upper() # 默认为 MD5# 1. 校验 Realmif client_realm != REALM:print(f"Realm mismatch: expected '{REALM}', got '{client_realm}'")return False# 2. 校验 Nonce# 检查 nonce 是否由服务器发出且未过期# 实际应用中,还需要检查 nc (nonce count) 以防止重放攻击 (nc 应该单调递增)# 这里简化处理:只检查 nonce 是否存在且未超时if client_nonce not in active_nonces:print(f"Invalid nonce: {client_nonce} (not issued by server)")# 可以考虑发送 stale=true,让客户端用新 nonce 重试return "stale" # 特殊返回值表示 nonce 过期if time.time() - active_nonces[client_nonce] > NONCE_LIFETIME_SECONDS:print(f"Nonce expired: {client_nonce}")del active_nonces[client_nonce] # 删除过期的 noncereturn "stale" # 特殊返回值表示 nonce 过期# 3. 校验 Opaque (如果服务器在挑战中发送了)if 'opaque' in auth_parts and auth_parts.get('opaque') != OPAQUE:print(f"Opaque mismatch")return False# 4. 获取用户密码 (或预计算的 HA1)password = USERS_PASSWORDS.get(username)if not password:print(f"Unknown user: {username}")return False# 5. 计算 HA1# HA1 = MD5(username:realm:password)ha1_str = f"{username}:{REALM}:{password}"ha1 = hashlib.md5(ha1_str.encode('utf-8')).hexdigest()print(f"Calculated HA1 for {username}: {ha1}")# 6. 计算 HA2# HA2 = MD5(method:uri)# 注意: self.command 是 HTTP 方法 (e.g., "GET")# uri 是客户端在 Authorization 头中提供的 URIha2_str = f"{self.command}:{uri}"ha2 = hashlib.md5(ha2_str.encode('utf-8')).hexdigest()print(f"Calculated HA2 for {self.command}:{uri}: {ha2}")# 7. 计算期望的 response# response = MD5(HA1:nonce:nc:cnonce:qop:HA2)if qop == "auth" or qop == "auth-int": # auth-int 需要校验 body,这里简化expected_response_str = f"{ha1}:{client_nonce}:{nc}:{cnonce}:{qop}:{ha2}"else:# 如果 qop 不存在 (较老的 RFC 2069 规范,requests 不会这样)expected_response_str = f"{ha1}:{client_nonce}:{ha2}"expected_response = hashlib.md5(expected_response_str.encode('utf-8')).hexdigest()print(f"Expected response: {expected_response}")print(f"Client response: {client_response}")# 8. 比较 responseif client_response == expected_response:# 认证成功后,可以考虑使当前 nonce 失效(或严格检查 nc)# del active_nonces[client_nonce] # 如果 nonce 只能使用一次return Trueelse:print("Response mismatch.")return False
此函数是服务端认证的核心逻辑,通过 8 步校验确保客户端的合法性:
- 参数完整性:检查必要参数(如
username
、nonce
)是否存在。 - realm 匹配:确保客户端请求的认证域与服务端配置一致。
- nonce 有效性:验证
nonce
是否由服务端生成且未过期(防止重放攻击)。 - opaque 校验:确保客户端未篡改挑战参数。
- 哈希计算:重新计算
HA1
(用户密码哈希)和HA2
(请求信息哈希),并生成期望的response
,与客户端提供的response
比较。
2.5 请求处理(do_GET 方法)
def do_GET(self):parsed_path = urlparse(self.path)# 只对 /sd 路径进行认证if parsed_path.path == '/sd':auth_header = self.headers.get('Authorization')if not auth_header:print("No Authorization header, sending 401 challenge.")self.send_401_challenge()returnauth_parts = parse_digest_auth_header(auth_header)if not auth_parts:print("Malformed Authorization header, sending 401 challenge.")self.send_401_challenge() # 或发送 400 Bad Requestreturnverification_result = self.verify_digest_response(auth_parts)if verification_result == "stale":print("Nonce was stale, sending 401 challenge with stale=true.")self.send_401_challenge(stale=True)elif verification_result:print("Authentication successful!")self.send_response(200)self.send_header('Content-type', 'application/json')self.end_headers()response_data = {"message": "Welcome to the secure data area!", "user": auth_parts.get('username')}import jsonself.wfile.write(json.dumps(response_data).encode('utf-8'))else:print("Authentication failed, sending 401 challenge again.")# 认证失败,可以简单地再次发送 401 (可能用新的 nonce)# 或者根据具体策略,如果尝试次数过多可以发送 403 Forbiddenself.send_401_challenge()else:self.send_response(200)self.send_header('Content-type', 'text/plain')self.end_headers()self.wfile.write(b"This is an open area.")
服务端通过 do_GET
方法处理请求:
- 若请求路径为
/sd
(受保护资源),则检查Authorization
头部。 - 无认证头或解析失败时,返回
401
挑战。 - 认证成功后返回资源(如示例中的 JSON 数据)。
- 其他路径(如根路径)直接返回公开内容。
三、关键技术点与安全增强
3.1 nonce 的时效性与重放攻击防范
- 时效性:
nonce
仅在NONCE_LIFETIME_SECONDS
(5 分钟)内有效,过期后服务端删除记录,客户端需重新获取新nonce
。 - 重放攻击:通过
nc
(请求计数)可以进一步防范 —— 客户端每次使用同一nonce
时,nc
必须递增(如从00000001
到00000002
),服务端若发现nc
未递增或重复,则判定为重放攻击。
3.2 密码存储的最佳实践
示例中直接存储明文密码(仅为演示),实际生产环境应存储预计算的 HA1
(MD5(username:realm:password)
)。这样即使数据库泄露,攻击者也无法直接获取密码明文,需结合 realm
和 username
才能计算 HA1
,进一步增强安全性。
3.3 与 Basic 认证的对比
特性 | Basic 认证 | Digest 认证 |
---|---|---|
密码传输方式 | Base64 编码明文(可逆) | 哈希值(不可逆) |
防重放攻击 | 不支持 | 支持(通过 nonce、nc) |
安全性 | 低(易被中间人截获明文) | 高(无明文传输,动态随机数) |
客户端支持 | 所有主流浏览器 | 所有主流浏览器 |
四、测试与验证
4.1 启动服务端
运行代码后,服务端监听 0.0.0.0:8001
,保护路径为 /sd
。
4.2 测试请求
使用 requests
模拟客户端请求:
res = requests.get('http://127.0.0.1:8001/sd',auth=HTTPDigestAuth('admin001', 'my_password_random_generation'))
print(f"Status: {res.status_code}")
print(f"Request Authorization: {res.request.headers['Authorization']}")
print(f"Headers: {res.headers}")
print(f"Response Text: {res.text}")
requests
客户端会自动处理挑战 - 响应流程,请求后打印,可见如下响应信息:
Status: 200
Request Authorization: Digest username="admin001", realm="MyProtectedSD", nonce="f4b50139aeb242406e92e3a24a14f286", uri="/sd", response="72c8e7cf046193a3bce3fb80ec1ce4f6", opaque="b5a7e1bf338b6f1d6c70204c64fd9473", algorithm="MD5", qop="auth", nc=00000001, cnonce="0e22129e06725aa6"
Headers: {'Server': 'BaseHTTP/0.6 Python/3.12.9', 'Date': 'Thu, 22 May 2025 08:41:52 GMT', 'Content-type': 'application/json'}
Response Text: {"message": "Welcome to the secure data area!", "user": "admin001"}
服务端打印关键信息,可见客户端请求流程及认证明细情况:
No Authorization header, sending 401 challenge.
127.0.0.1 - "GET /sd HTTP/1.1" 401 -
Sent 401 challenge with new nonce: f4b50139aeb242406e92e3a24a14f286
Calculated HA1 for admin001: fb5c1f99227711de645d65e5b091f978
Calculated HA2 for GET:/sd: 7c7e535b35fb1070562dec4be2da7ee5
Expected response: 72c8e7cf046193a3bce3fb80ec1ce4f6
Client response: 72c8e7cf046193a3bce3fb80ec1ce4f6
Authentication successful!
127.0.0.1 - "GET /sd HTTP/1.1" 200 -
若认证成功,服务端返回文本:
{"message": "Welcome to the secure data area!", "user": "admin001"}
4.3 验证 nonce 过期
等待 5 分钟后,若使用之前的 nonce
再次请求,服务端会返回 stale=true
的挑战头,提示客户端使用新 nonce
。
五、服务端实现整体代码
import hashlib
import time
import os
import re
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse# --- 配置 ---
SERVER_ADDRESS = '0.0.0.0' # 监听所有接口,方便测试
SERVER_PORT = 8001
REALM = "MyProtectedSD" # 保护域名,会显示在客户端的认证提示中# 存储用户名和密码(在实际应用中,密码应该哈希存储,这里为了演示方便直接存储)
# 或者更好的方式是存储 HA1 = MD5(username:realm:password)
# 例如: HA1 = hashlib.md5(f"your_username_here:{REALM}:your_password_here".encode()).hexdigest()
USERS_PASSWORDS = {"admin001": "my_password_random_generation"
}# 存储已发出的 nonce 及其创建时间,用于校验和防止重放
# {nonce_value: creation_timestamp}
# 实际应用中,nonce 应该有过期机制,并且用后即焚或严格校验 nc (nonce count)
active_nonces = {}
NONCE_LIFETIME_SECONDS = 300 # Nonce 有效期,例如 5 分钟# Opaque 值,服务器生成,客户端原样返回
OPAQUE = hashlib.md5(os.urandom(16)).hexdigest()def generate_nonce():"""生成一个唯一的 nonce 并记录创建时间"""nonce = hashlib.md5((os.urandom(16).hex() + str(time.time())).encode()).hexdigest()active_nonces[nonce] = time.time() # 记录 nonce 创建时间return noncedef parse_digest_auth_header(auth_header_value):"""解析 Authorization: Digest ... 头部字符串返回一个包含各参数的字典"""if not auth_header_value or not auth_header_value.lower().startswith('digest '):return Noneauth_parts = {}# 移除 "Digest " 前缀value_str = auth_header_value[len('Digest '):]# 使用正则表达式解析 key="value" 或 key=value 对# 这个正则表达式处理了带引号和不带引号的值pattern = re.compile(r'(\w+)=(?:"([^"]*)"|([^\s,]*))')for match in pattern.finditer(value_str):key = match.group(1)# 值可能在 group(2) (带引号) 或 group(3) (不带引号)val = match.group(2) if match.group(2) is not None else match.group(3)auth_parts[key] = valreturn auth_partsclass DigestAuthHandler(BaseHTTPRequestHandler):def send_401_challenge(self, stale=False):"""发送 401 Unauthorized 响应和 WWW-Authenticate 挑战头"""self.send_response(401)self.send_header('Content-type', 'text/plain')nonce = generate_nonce() # 生成新 nonceauth_challenge = f'Digest realm="{REALM}", qop="auth", nonce="{nonce}", opaque="{OPAQUE}", algorithm="MD5"'if stale:auth_challenge += ', stale="true"' # 标记旧 nonce 过期,提示客户端使用新 nonceself.send_header('WWW-Authenticate', auth_challenge)self.end_headers()self.wfile.write(b"Authentication required.")print(f"Sent 401 challenge with new nonce: {nonce}")def verify_digest_response(self, auth_parts):"""校验客户端提供的 Digest 认证信息"""required_keys = ['username', 'realm', 'nonce', 'uri', 'response', 'qop', 'nc', 'cnonce']for key in required_keys:if key not in auth_parts:print(f"Missing digest auth key: {key}")return Falseusername = auth_parts.get('username')client_realm = auth_parts.get('realm')client_nonce = auth_parts.get('nonce')uri = auth_parts.get('uri')client_response = auth_parts.get('response')qop = auth_parts.get('qop')nc = auth_parts.get('nc') # nonce countcnonce = auth_parts.get('cnonce') # client noncealgorithm = auth_parts.get('algorithm', 'MD5').upper() # 默认为 MD5# 1. 校验 Realmif client_realm != REALM:print(f"Realm mismatch: expected '{REALM}', got '{client_realm}'")return False# 2. 校验 Nonce# 检查 nonce 是否由服务器发出且未过期# 实际应用中,还需要检查 nc (nonce count) 以防止重放攻击 (nc 应该单调递增)# 这里简化处理:只检查 nonce 是否存在且未超时if client_nonce not in active_nonces:print(f"Invalid nonce: {client_nonce} (not issued by server)")# 可以考虑发送 stale=true,让客户端用新 nonce 重试return "stale" # 特殊返回值表示 nonce 过期if time.time() - active_nonces[client_nonce] > NONCE_LIFETIME_SECONDS:print(f"Nonce expired: {client_nonce}")del active_nonces[client_nonce] # 删除过期的 noncereturn "stale" # 特殊返回值表示 nonce 过期# 3. 校验 Opaque (如果服务器在挑战中发送了)if 'opaque' in auth_parts and auth_parts.get('opaque') != OPAQUE:print(f"Opaque mismatch")return False# 4. 获取用户密码 (或预计算的 HA1)password = USERS_PASSWORDS.get(username)if not password:print(f"Unknown user: {username}")return False# 5. 计算 HA1# HA1 = MD5(username:realm:password)ha1_str = f"{username}:{REALM}:{password}"ha1 = hashlib.md5(ha1_str.encode('utf-8')).hexdigest()print(f"Calculated HA1 for {username}: {ha1}")# 6. 计算 HA2# HA2 = MD5(method:uri)# 注意: self.command 是 HTTP 方法 (e.g., "GET")# uri 是客户端在 Authorization 头中提供的 URIha2_str = f"{self.command}:{uri}"ha2 = hashlib.md5(ha2_str.encode('utf-8')).hexdigest()print(f"Calculated HA2 for {self.command}:{uri}: {ha2}")# 7. 计算期望的 response# response = MD5(HA1:nonce:nc:cnonce:qop:HA2)if qop == "auth" or qop == "auth-int": # auth-int 需要校验 body,这里简化expected_response_str = f"{ha1}:{client_nonce}:{nc}:{cnonce}:{qop}:{ha2}"else:# 如果 qop 不存在 (较老的 RFC 2069 规范,requests 不会这样)expected_response_str = f"{ha1}:{client_nonce}:{ha2}"expected_response = hashlib.md5(expected_response_str.encode('utf-8')).hexdigest()print(f"Expected response: {expected_response}")print(f"Client response: {client_response}")# 8. 比较 responseif client_response == expected_response:# 认证成功后,可以考虑使当前 nonce 失效(或严格检查 nc)# del active_nonces[client_nonce] # 如果 nonce 只能使用一次return Trueelse:print("Response mismatch.")return Falsedef do_GET(self):parsed_path = urlparse(self.path)# 只对 /sd 路径进行认证if parsed_path.path == '/sd':auth_header = self.headers.get('Authorization')if not auth_header:print("No Authorization header, sending 401 challenge.")self.send_401_challenge()returnauth_parts = parse_digest_auth_header(auth_header)if not auth_parts:print("Malformed Authorization header, sending 401 challenge.")self.send_401_challenge() # 或发送 400 Bad Requestreturnverification_result = self.verify_digest_response(auth_parts)if verification_result == "stale":print("Nonce was stale, sending 401 challenge with stale=true.")self.send_401_challenge(stale=True)elif verification_result:print("Authentication successful!")self.send_response(200)self.send_header('Content-type', 'application/json')self.end_headers()response_data = {"message": "Welcome to the secure data area!", "user": auth_parts.get('username')}import jsonself.wfile.write(json.dumps(response_data).encode('utf-8'))else:print("Authentication failed, sending 401 challenge again.")# 认证失败,可以简单地再次发送 401 (可能用新的 nonce)# 或者根据具体策略,如果尝试次数过多可以发送 403 Forbiddenself.send_401_challenge()else:self.send_response(200)self.send_header('Content-type', 'text/plain')self.end_headers()self.wfile.write(b"This is an open area.")def log_message(self, format, *args):"""覆盖默认日志,方便调试"""print(f"{self.address_string()} - {format % args}")def run_server(server_class=HTTPServer, handler_class=DigestAuthHandler, addr=SERVER_ADDRESS, port=SERVER_PORT):server_address = (addr, port)httpd = server_class(server_address, handler_class)print(f"Starting Digest Auth server on {addr}:{port}...")print(f"Protected path: /sd")print(f"Test with username: '{USERS_PASSWORDS.keys()}', password: '{USERS_PASSWORDS.values()}'")try:httpd.serve_forever()except KeyboardInterrupt:print("\nServer shutting down.")finally:httpd.server_close()if __name__ == '__main__':run_server()
小总结
HTTP Digest 认证通过哈希算法和动态随机数(nonce)解决了 Basic 认证的明文传输问题,是轻量级场景下的安全认证方案。本文结合代码详细解析了其核心流程(挑战 - 响应)和服务端实现逻辑(nonce 管理、哈希计算、响应验证),并强调了生产环境中的安全增强点(如存储 HA1
、校验 nc
)。实际应用中,建议结合 HTTPS 进一步加密传输过程,以达到更高的安全性。
————————————————
Java猿社区—Http digest authentication 请求代码最全示例 - 简书
HTTP认证之摘要认证——Digest(二) - xiaoxiaotank - 博客园
HTTP的几种认证方式之DIGEST 认证(摘要认证) - wenbin_ouyang - 博客园
HTTP Authentication之Basic认证、Digest认证
http digest鉴权流程
python http 身份认证简介