对接第三方的平台时碰到需要使用需要用的Digest SHA256 WWW-Authenticate认证方式,具体代码如下
public class DigestAuthenticator
{
private readonly HttpClient _httpClient;
private readonly string _username;
private readonly string _password;
private readonly bool _enableLogging;
public DigestAuthenticator(HttpClient httpClient, string username, string password, bool enableLogging = true)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_username = username ?? throw new ArgumentNullException(nameof(username));
_password = password ?? throw new ArgumentNullException(nameof(password));
_enableLogging = enableLogging;
}
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
// 首次请求
var response = await _httpClient.SendAsync(request);
Log($"首次响应状态: {response.StatusCode}");
if (response.StatusCode != HttpStatusCode.Unauthorized)
return response;
// 解析 WWW-Authenticate 头
var authHeader = response.Headers.WwwAuthenticate.ToString();
Log($"WWW-Authenticate: {authHeader}");
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Digest", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("服务器未返回正确的 Digest 认证信息");
var digestParams = ParseDigestParams(authHeader);
LogParsedParams(digestParams);
string qopRaw = digestParams.GetValueOrDefault("qop", "")?.Trim();
bool isQopEnabled = qopRaw == "auth" || qopRaw == "auth-int";
Log($"qop 原始值: '{qopRaw}', 是否启用: {isQopEnabled}");
// 读取请求体(auth-int 时需要)
byte[] bodyBytes = null;
if (request.Content != null && qopRaw == "auth-int")
{
bodyBytes = await request.Content.ReadAsByteArrayAsync();
Log($"读取请求体,长度: {bodyBytes.Length}");
}
// 构造 Authorization 头值
string authValue = BuildAuthorizationHeader(
request.Method.Method,
request.RequestUri,
digestParams,
bodyBytes,
isQopEnabled,
qopRaw);
// 创建新请求(不能重用原请求)
var newRequest = new HttpRequestMessage(request.Method, request.RequestUri);
foreach (var header in request.Headers)
{
if (header.Key == "Authorization") continue;
newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (request.Content != null)
{
if (bodyBytes == null)
bodyBytes = await request.Content.ReadAsByteArrayAsync();
newRequest.Content = new ByteArrayContent(bodyBytes);
foreach (var header in request.Content.Headers)
{
newRequest.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
Log($"已复制请求体,长度: {bodyBytes.Length}");
}
newRequest.Headers.Authorization = new AuthenticationHeaderValue("Digest", authValue);
// 重试
var retryResponse = await _httpClient.SendAsync(newRequest);
Log($"重试响应状态: {retryResponse.StatusCode}");
return retryResponse;
}
public async Task<HttpResponseMessage> SendAsync(string method, string url, string body = null, string contentType = "application/json")
{
var request = new HttpRequestMessage(new HttpMethod(method), url);
if (!string.IsNullOrEmpty(body))
{
request.Content = new StringContent(body, Encoding.UTF8, contentType);
}
return await SendAsync(request);
}
private void Log(string message)
{
if (_enableLogging)
Console.WriteLine($"[Digest] {message}");
}
private void LogParsedParams(Dictionary<string, string> dict)
{
if (!_enableLogging) return;
foreach (var kv in dict)
Console.WriteLine($"[Digest] 解析参数: {kv.Key} = {kv.Value}");
}
private Dictionary<string, string> ParseDigestParams(string authHeader)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// 移除 "Digest " 前缀
string parameters = authHeader.Substring(7).Trim();
// 按逗号分割参数
var parts = parameters.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var kv = part.Split(new[] { '=' }, 2);
if (kv.Length != 2) continue;
string key = kv[0].Trim();
string value = kv[1].Trim();
// 去除值两端的双引号
if (value.StartsWith("\"") && value.EndsWith("\""))
value = value.Substring(1, value.Length - 2);
dict[key] = value;
}
return dict;
}
private string BuildAuthorizationHeader(
string method,
Uri uri,
Dictionary<string, string> digestParams,
byte[] bodyBytes,
bool isQopEnabled,
string qop)
{
string realm = digestParams.GetValueOrDefault("realm", "");
string nonce = digestParams.GetValueOrDefault("nonce", "");
string algorithm = digestParams.GetValueOrDefault("algorithm", "MD5");
string opaque = digestParams.GetValueOrDefault("opaque", "");
string uriStr = uri.PathAndQuery;
Log($"realm={realm}, nonce={nonce}, algorithm={algorithm}, opaque={opaque}");
var hashType = algorithm.Equals("SHA-256", StringComparison.OrdinalIgnoreCase) ? HashType.SHA256 : HashType.MD5;
// HA1 = H(username:realm:password)
string ha1 = ComputeHash($"{_username}:{realm}:{_password}", hashType);
Log($"HA1: {ha1}");
string ha2;
if (isQopEnabled && qop == "auth-int")
{
string bodyHash = (bodyBytes == null || bodyBytes.Length == 0)
? ComputeHash("", hashType)
: ComputeHash(Encoding.UTF8.GetString(bodyBytes), hashType);
ha2 = ComputeHash($"{method}:{uriStr}:{bodyHash}", hashType);
Log($"HA2 (auth-int): {ha2}, bodyHash: {bodyHash}");
}
else
{
ha2 = ComputeHash($"{method}:{uriStr}", hashType);
Log($"HA2: {ha2}");
}
string response;
string nc = null;
string cnonce = null;
if (!isQopEnabled)
{
response = ComputeHash($"{ha1}:{nonce}:{ha2}", hashType);
Log($"简化模式 response: {response}");
}
else
{
cnonce = GenerateCnonce();
nc = "00000001";
response = ComputeHash($"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}", hashType);
Log($"标准模式 response: {response}, nc={nc}, cnonce={cnonce}");
}
var parts = new List<string>
{
$"username=\"{_username}\"",
$"realm=\"{realm}\"",
$"nonce=\"{nonce}\"",
$"uri=\"{uriStr}\"",
$"algorithm=\"{algorithm}\"",
$"response=\"{response}\""
};
if (isQopEnabled)
{
parts.Add($"qop=\"{qop}\"");
parts.Add($"nc={nc}");
parts.Add($"cnonce=\"{cnonce}\"");
}
if (!string.IsNullOrEmpty(opaque))
parts.Add($"opaque=\"{opaque}\"");
string authValue = string.Join(", ", parts);
Log($"Authorization: Digest {authValue}");
return authValue;
}
private string GenerateCnonce()
{
byte[] bytes = new byte[16];
using (var rng = RandomNumberGenerator.Create())
rng.GetBytes(bytes);
return BitConverter.ToString(bytes).Replace("-", "").ToLower();
}
private enum HashType { MD5, SHA256 }
private string ComputeHash(string input, HashType type)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes;
if (type == HashType.MD5)
{
using (var md5 = MD5.Create())
hashBytes = md5.ComputeHash(inputBytes);
}
else
{
using (var sha256 = SHA256.Create())
hashBytes = sha256.ComputeHash(inputBytes);
}
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
}调用方式如下
static async Task Main(string[] args)
{
// 设备地址和认证信息
string baseUrl = "http://localhost:8080";
string username = "admin";
string password = "admin12345";
using var handler = new HttpClientHandler();
using var client = new HttpClient(handler);
var authenticator = new DigestAuthenticator(client, username, password, enableLogging: true);
// 示例1:GET 请求(无 body)
string getUrl = $"{baseUrl}/iot/global/0-global/model/attribute/get/Authentication/UserCheck";
var getResponse = await authenticator.SendAsync("GET", getUrl);
string getContent = await getResponse.Content.ReadAsStringAsync();
Console.WriteLine($"GET 状态: {getResponse.StatusCode}");
Console.WriteLine($"GET 响应: {getContent}\n");
// 示例2:POST 请求(带 JSON body)
string postUrl = $"{baseUrl}/iot/global/0-global/model/service/operate/SearchRecord/SearchRecordResult";
string jsonBody = @"
{
""searchID"": ""37c9c2f4-3879-488a-86b7"",
""searchResultPosition"": 1,
""maxResults"": 100,
""streamItem"": ""mainStream"",
""ChannelList"": [{ ""channel"": 1 }],
""TimeSpan"": {
""startTime"": ""2020-03-12T10:10:05+08:00"",
""endTime"": ""2020-03-12T12:10:05+08:00""
},
""recordType"": ""all""
}";
var postResponse = await authenticator.SendAsync("POST", postUrl, jsonBody, "application/json");
string postContent = await postResponse.Content.ReadAsStringAsync();
Console.WriteLine($"POST 状态: {postResponse.StatusCode}");
Console.WriteLine($"POST 响应: {postContent}");
}