类别
标签
DigestAuthenticator授权认证封装类

对接第三方的平台时碰到需要使用需要用的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}");
        }