You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python Requests Session复用HTTPDigestAuth认证异常问题咨询

解决HTTP Digest认证会话重复认证问题

问题根源

requests库自带的HTTPDigestAuth是无状态实现,每次请求都会重新触发完整的认证流程,不会在Session实例中保存nc(nonce计数)、cnonce等关键认证状态,因此即便使用Session,后续请求仍会重复认证,nc值始终停留在1。

解决方案:实现带状态的Digest认证类

我们可以基于requests的HTTPDigestAuth扩展,让它在Session内持久化认证状态,自动递增nc并复用已有认证信息,避免重复认证。

代码实现

import requests
from requests.auth import HTTPDigestAuth
import hashlib
import time

class StatefulDigestAuth(HTTPDigestAuth):
    def __init__(self, username, password):
        super().__init__(username, password)
        self.nonce_count = 0
        self.cnonce = None
        self.auth_state = {}  # 存储realm/nonce/opaque/qop等状态

    def handle_401(self, r, **kwargs):
        # 解析响应中的Digest认证头,保存状态
        auth_header = r.headers.get('WWW-Authenticate', '')
        if 'Digest' in auth_header:
            self.auth_state = self._parse_auth_header(auth_header)
        
        # 更新认证状态计数与随机值
        self.nonce_count += 1
        self.cnonce = self.cnonce or hashlib.md5(str(time.time()).encode()).hexdigest()[:16]
        
        # 重新构建认证头并发送请求
        req = r.request
        req.headers['Authorization'] = self._build_auth_header(req.method, req.url)
        r.content
        r.close()
        new_r = r.connection.send(req, **kwargs)
        new_r.history.append(r)
        new_r.request = req
        return new_r

    def _parse_auth_header(self, header):
        # 把Digest头解析为键值对字典
        auth_dict = {}
        for part in header.split(','):
            if '=' in part:
                key, val = part.strip().split('=', 1)
                auth_dict[key.lower()] = val.strip('"')
        return auth_dict

    def _build_auth_header(self, method, url):
        # 计算Digest认证所需的HA1/HA2/response值
        ha1 = self._calc_ha1()
        ha2 = self._calc_ha2(method, url)
        response = self._calc_response(ha1, ha2)
        
        # 拼接完整的认证头字符串
        auth_parts = [
            f'Digest username="{self.username}"',
            f'realm="{self.auth_state["realm"]}"',
            f'nonce="{self.auth_state["nonce"]}"',
            f'uri="{url.split("?")[0]}"',
            f'response="{response}"',
            f'cnonce="{self.cnonce}"',
            f'nc={self.nonce_count:08x}',
            f'qop={self.auth_state.get("qop", "auth")}'
        ]
        if self.auth_state.get('opaque'):
            auth_parts.append(f'opaque="{self.auth_state["opaque"]}"')
        if self.auth_state.get('algorithm'):
            auth_parts.append(f'algorithm={self.auth_state["algorithm"]}')
        return ', '.join(auth_parts)

    def _calc_ha1(self):
        # 计算HA1值,兼容MD5和MD5-sess算法
        base = f"{self.username}:{self.auth_state['realm']}:{self.password}".encode()
        ha1 = hashlib.md5(base).hexdigest()
        if self.auth_state.get('algorithm') == 'MD5-sess':
            ha1 = hashlib.md5(f"{ha1}:{self.auth_state['nonce']}:{self.cnonce}".encode()).hexdigest()
        return ha1

    def _calc_ha2(self, method, url):
        # 计算HA2值,兼容auth和auth-int模式
        if self.auth_state.get('qop') == 'auth-int':
            ha2 = hashlib.md5(f"{method}:{url}:".encode()).hexdigest()  # 若有请求体需替换为空字符串
        else:
            ha2 = hashlib.md5(f"{method}:{url.split('?')[0]}".encode()).hexdigest()
        return ha2

    def _calc_response(self, ha1, ha2):
        # 计算最终的response签名值
        qop = self.auth_state.get('qop')
        if qop:
            return hashlib.md5(f"{ha1}:{self.auth_state['nonce']}:{self.nonce_count:08x}:{self.cnonce}:{qop}:{ha2}".encode()).hexdigest()
        else:
            return hashlib.md5(f"{ha1}:{self.auth_state['nonce']}:{ha2}".encode()).hexdigest()

使用方法

将原代码中的HTTPDigestAuth替换为自定义的StatefulDigestAuth,配合Session使用即可:

with requests.Session() as session:
    # 初始化带状态的认证实例
    session.auth = StatefulDigestAuth('你的用户名', '你的密码')
    
    # 第一次请求(触发401认证)
    resp1 = session.get('https://你的API地址/端点')
    print(resp1.request.headers['Authorization'])  # 可看到nc=00000001
    
    # 第二次请求(复用认证状态,nc自动递增)
    resp2 = session.get('https://你的API地址/端点')
    print(resp2.request.headers['Authorization'])  # 可看到nc=00000002

简化GET请求流程

基于Session和自定义认证类,你可以把请求逻辑封装成简单函数,避免重复编写请求代码:

def call_api(session, url):
    try:
        resp = session.get(url)
        resp.raise_for_status()
        return resp.json()
    except requests.exceptions.RequestException as e:
        print(f"请求失败: {str(e)}")
        return None

# 批量请求示例
with requests.Session() as session:
    session.auth = StatefulDigestAuth('user', 'pass')
    target_urls = [
        'https://api.example.com/resource1',
        'https://api.example.com/resource2',
        'https://api.example.com/resource3'
    ]
    results = [call_api(session, url) for url in target_urls]

验证效果

多次请求后查看Authorization头,会发现nc值从00000001开始逐次递增,说明认证状态已被正确维持,不会重复发起认证握手。

内容的提问来源于stack exchange,提问作者The Frog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 01:25:07