本文最后更新于 604 天前,其中的信息可能已经有所发展或是发生改变。
故事是这样的:
前些日子接了一个对接上传接口的活,里面的接口有个叫” 注册接口 “、” 保活接口 “……,里面并没有说明密码如何传输。于是我截图问对方,如何写?对方直接甩了一句” 百度下摘要认证,国际通用的认证方式 “。于是我查资料,恶补。
相关资料:
HTTP 认证之摘要认证 ——Digest(一) – xiaoxiaotank – 博客园 (cnblogs.com)
HTTP 的几种认证方式之 DIGEST 认证(摘要认证) – wenbin_ouyang – 博客园 (cnblogs.com)
总结就是:
《注册》:
第一步:向指定接口发送一条空请求信息,然后获取返回的 401 信息中 header 中的内容。
【客户端】- 请求 ->【服务端】
【客户端】<- 返回 401、header 值 -【服务端】
第二步:将 header 中获取到的内容和密码进行 MD5 加密组合,再次发送一条带加密 header 的请求,然后得到返回结果。
【客户端】- 加密 header 值 + 密码,传入 header 请求 ->【服务端】
【客户端】<- 成功信息 -【服务端】
《传输》:
第三步:以返回成功的 header 中的几个值为准,对后续传输时用这几个值进行加密。
返回的 header 中几个值的意义:
WWW-Authentication
:用来定义使用何种方式(Basic、Digest、Bearer 等)去进行认证以获取受保护的资源realm
:表示 Web 服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码qop
:保护质量,包含auth
(默认的)和auth-int
(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值nonce
:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击nc
:nonce 计数器,是一个 16 进制的数值,表示同一 nonce 下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送 “nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求cnonce
:客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护response
:这是由用户代理软件计算出的一个字符串,以证明用户知道口令Authorization-Info
:用于返回一些与授权会话相关的附加信息nextnonce
:下一个服务端随机数,使客户端可以预先发送正确的摘要rspauth
:响应摘要,用于客户端对服务端进行认证stale
:当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的 401 响应,并指定stale=true
,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了
上代码(C# 为例):
首先创建一个用于解析和创建 header 的类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Security.Cryptography; using System.Security.Principal; namespace InformationUpload.Digest { internal class DigestUtils { public static string GenerateToken() { string s = $"{DateTime.Now.Ticks}{new Random().Next()}"; using (MD5 md5 = MD5.Create()) { byte[] inputBytes = Encoding.ASCII.GetBytes(s); byte[] hashBytes = md5.ComputeHash(inputBytes); return Convert.ToBase64String(hashBytes); } } public static string CalculateMD5(string input) { using (MD5 md5 = MD5.Create()) { byte[] inputBytes = Encoding.ASCII.GetBytes(input); byte[] hashBytes = md5.ComputeHash(inputBytes); StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.Length; i++) { sb.Append(hashBytes[i].ToString("x2")); } return sb.ToString(); } } public static DigestAuthInfo GetAuthInfoObject(string authStr) { if (string.IsNullOrEmpty(authStr) || authStr.Length <= 7) return null; if (authStr.ToLower().IndexOf("digest") >= 0) { // 截掉前缀 Digest authStr = authStr.Substring(6); } // 将双引号去掉 authStr = authStr.Replace("\"", ""); DigestAuthInfo digestAuthObject = new DigestAuthInfo(); string[] authArray = authStr.Split(','); foreach (string auth in authArray) { string[] parts = auth.Trim().Split('='); if (parts.Length == 2) { string key = parts[0].Trim(); string value = parts[1].Trim(); switch (key) { case "username": digestAuthObject.Username = value; break; case "realm": digestAuthObject.Realm = value; break; case "nonce": digestAuthObject.Nonce = value; break; case "uri": digestAuthObject.Uri = value; break; case "response": digestAuthObject.Response = value; break; case "qop": digestAuthObject.Qop = value; break; case "nc": digestAuthObject.Nc = value; break; case "cnonce": digestAuthObject.Cnonce = value; break; case "opaque": digestAuthObject.Opaque = value; break; case "User-Identify": digestAuthObject.UI = value; break; } } } return digestAuthObject; } } public class DigestAuthInfo { public string Username { get; set; } public string Realm { get; set; } public string Nonce { get; set; } public string Uri { get; set; } public string Response { get; set; } public string Qop { get; set; } public string Nc { get; set; } public string Cnonce { get; set; } public string Opaque { get; set; } public string UI { get; set; } public string authorizationString() { return $"Digest username=\"{Username}\", realm=\"{Realm}\", nonce=\"{Nonce}\", uri=\"{Uri}\", response=\"{Response}\", opaque=\"{Opaque}\", qop=\"{Qop}\", nc=\"{Nc}\", cnonce=\"{Cnonce}\""; } } }
主要加密方式:MD5(MD5(<username>:<realm>:<password>):<nonce>:<nc>:<cnonce>:<qop>:MD5(<request-method>:<uri>))
然后对 http 请求:
using InformationUpload.Digest; using InformationUpload.JsonModel; using InformationUpload.Model; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Data.SqlClient; using System.IO; using System.Linq; using System.Net; using System.Net.Mime; using System.Security.Policy; using System.Text; using System.Windows.Forms.VisualStyles; using System.Windows.Forms; namespace InformationUpload.Controller { internal class DigestController { static string iniPath = Application.StartupPath.ToString() + @"\Config.ini"; static IniFiles ini = new IniFiles(iniPath); // 接口http private static string HttpUrl = ini.IniReadValue("合作商配置", "Url"); // User-Identify 域和鉴权信息 private static string UI = ini.IniReadValue("合作商配置", "UI"); private static string Username = ini.IniReadValue("合作商配置", "Username"); private static string Password = ini.IniReadValue("合作商配置", "Password"); private static string OtherRealm = "acsncgVIIDExt"; // 定义要发送的JSON数据 private static string GetRegisterObject() { RegisterRequestData registerRequestData = new RegisterRequestData { RegisterObject = new RegisterObject { DeviceID = UI } }; return JsonConvert.SerializeObject(registerRequestData); } private static string GetKeepaliveObject() { KeepaliveRequestData keepaliveRequestData = new KeepaliveRequestData { KeepaliveObject = new KeepaliveObject { DeviceID = UI } }; return JsonConvert.SerializeObject(keepaliveRequestData); } // nc初始值 private static int nc = 0; /// <summary> /// 注册消息 /// </summary> /// <returns></returns> public static string[] SystemRegister() { string[] returnArray = new string[3]; Form1.form.outPut("=第一次请求:"); string uri = "/VIID/System/Register"; try { string[] firstRequest = SendRequest($"{HttpUrl}{uri}", GetRegisterObject()); if (firstRequest[0] == "Unauthorized") { Form1.form.outPut("=第一次请求返回的 WWW-Authenticate: " + firstRequest[1]); nc = 0; string authorization = PostAuthorization("SystemRegister", uri, firstRequest[1]); Form1.form.outPut("=第二次请求:"); string[] secondRequest = SendRequest($"{HttpUrl}{uri}", GetRegisterObject(), authorization); if (secondRequest[0] == "OK") { Form1.form.outPut("=第二次请求返回成功"); RegisterResponseData registerResponseData = JsonConvert.DeserializeObject<RegisterResponseData>(secondRequest[2]); if (registerResponseData.ResponseStatusObject.StatusString == "success") { returnArray[0] = "success"; returnArray[1] = firstRequest[1]; returnArray[2] = nc.ToString(); } else { returnArray[0] = "error"; returnArray[1] = firstRequest[2]; } } else { returnArray[0] = "error"; returnArray[1] = "对方服务器未返回结果2:" + secondRequest[0]; ; } } else { returnArray[0] = "error"; returnArray[1] = "对方服务器未返回结果1:" + firstRequest[0]; } } catch (Exception ex) { Form1.form.outPut("=第一次请求失败:" + ex.Message); returnArray[0] = "error"; returnArray[1] = ex.Message; } return returnArray; } /// <summary> /// 保活接口 /// </summary> /// <param name="firstResponseWwwAuthenticate">注册时第一次返回的 WWW-Authenticate</param> /// <param name="nowNc">现在的 Nc 值</param> /// <returns></returns> public static string[] SystemKeepalive(string firstResponseWwwAuthenticate) { Form1.form.outPut("=SystemKeepalive:"); string[] returnArray = new string[3]; string uri = "/VIID/System/Keepalive"; string authorization = PostAuthorization("SystemKeepalive", uri, firstResponseWwwAuthenticate, OtherRealm); string[] keepaliveRequest = SendRequest($"{HttpUrl}{uri}", GetKeepaliveObject(), authorization); if (keepaliveRequest[0] == "OK") { KeepaliveResponseData keepaliveResponseData = JsonConvert.DeserializeObject<KeepaliveResponseData>(keepaliveRequest[2]); if (keepaliveResponseData.ResponseStatusObject.StatusString == "success") { Form1.form.outPut("=活动中"); returnArray[0] = "success"; returnArray[1] = keepaliveRequest[1]; returnArray[2] = nc.ToString(); } } else { returnArray[0] = "error"; returnArray[1] = "对方服务器未返回结果:" + keepaliveRequest[0]; } return returnArray; } /// <summary> /// 数据传输接口 /// </summary> /// <param name="firstResponseWwwAuthenticate">第一次传送的WwwAuthenticate</param> /// <param name="transferData">发送的 json 数据</param> /// <param name="nowNc">现在的 nc</param> /// <returns>[0]:返回的状态码;[1]:当前的WwwAuthenticate;[2]:当前的nc;[3]:正常返回的结果</returns> public static DataTransferResultObject DataTransfer(string firstResponseWwwAuthenticate, string transferData, string nowNc) { DataTransferResultObject resultObject = new DataTransferResultObject(); string uri = "/VIID/DataTransfer"; nc = Convert.ToInt32(nowNc); string[] keepaliveMessage = DigestController.SystemKeepalive(firstResponseWwwAuthenticate); if (keepaliveMessage[0] != "success") { Form1.form.outPut("保活不成功"); string[] registerMessage = DigestController.SystemRegister(); if (registerMessage[0] == "success") { Form1.form.outPut("重新注册成功"); firstResponseWwwAuthenticate = registerMessage[1]; nowNc = registerMessage[2]; nc = Convert.ToInt32(nowNc); } else { resultObject.Status = "error"; resultObject.FRWA = firstResponseWwwAuthenticate; resultObject.NowNC = nc.ToString(); return resultObject; } } else Form1.form.outPut("保活成功"); string dataTransferAuthorization = PostAuthorization("DataTransfer", uri, firstResponseWwwAuthenticate, OtherRealm); Form1.form.outPut("发送数据中..."); string[] dataTransferResult = SendRequest($"{HttpUrl}{uri}", transferData, dataTransferAuthorization, true); if (dataTransferResult[0] == "OK") { Form1.form.outPut("发送成功", 0); resultObject.Data = dataTransferResult[2]; } Form1.form.outPut("发送结束"); resultObject.Status = dataTransferResult[0]; resultObject.FRWA = firstResponseWwwAuthenticate; resultObject.NowNC = nc.ToString(); return resultObject; } /// <summary> /// 发送请求 /// </summary> /// <param name="url">请求地址</param> /// <param name="data">请求数据</param> /// <param name="authorization">请求头 Authorization,第一次默认为空</param> /// <returns>[0]:返回的状态码;[1]:返回的Www-Authenticate;[2]:返回结果</returns> private static string[] SendRequest(string url, string data, string authorization = null, bool bUI = false) { // DataTransferResultObject resultObject = new DataTransferResultObject(); string[] returnArray = new string[3]; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); request.Method = "POST"; request.ContentType = "application/json"; request.Accept = "*/*"; // 添加头Authorization信息 if (!string.IsNullOrEmpty(authorization)) { request.Headers.Add("Authorization", authorization); } if (bUI) { request.Headers.Add("User-Identify", UI); } Form1.form.outPut("NC:" + nc); Form1.form.outPut("URL:" + url, 0); Form1.form.outPut("Data:" + data, 0); Form1.form.outPut("Authorization:" + authorization, 0); // 添加请求内容 using (var streamWriter = new StreamWriter(request.GetRequestStream())) { streamWriter.Write(data); streamWriter.Flush(); } try { using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { returnArray[0] = response.StatusCode.ToString(); returnArray[1] = response.Headers["WWW-Authenticate"]; returnArray[2] = reader.ReadToEnd(); // Form1.form.outPut("请求成功返回的WWW-Authenticate内容: " + returnArray[1]); } return returnArray; } catch (WebException ex) { Form1.form.outPut("请求失败:" + ex.Message); if (ex.Response is HttpWebResponse errorResponse) { if (errorResponse.StatusCode == HttpStatusCode.Unauthorized) { using (StreamReader errorReader = new StreamReader(errorResponse.GetResponseStream(), Encoding.UTF8)) { returnArray[0] = errorResponse.StatusCode.ToString(); returnArray[1] = errorResponse.Headers["WWW-Authenticate"]; returnArray[2] = errorReader.ReadToEnd(); } return returnArray; } } throw ex; } } /// <summary> /// 请求头 Authorization 生成 /// </summary> /// <param name="postType">post 类型 </param> /// <param name="uri">uri 地址</param> /// <param name="wwwAuthenticate">WWW-Authenticate 内容</param> /// <param name="realm">需要组合的 realm 值 第一次默认为空, 即 viid</param> /// <returns></returns> private static string PostAuthorization(string postType, string uri, string wwwAuthenticate, string realm = null) { DigestAuthInfo authObject = DigestUtils.GetAuthInfoObject(wwwAuthenticate); // nc 递增 nc++; authObject.Username = Username; authObject.Nc = nc.ToString("X8"); // 转换为十六进制字符串 authObject.Cnonce = DigestUtils.GenerateToken(); // 生成随机值 authObject.Uri = uri; authObject.UI = UI; if (!string.IsNullOrEmpty(realm)) { authObject.Realm = realm; } // MD5(MD5(<username>:<realm>:<password>):<nonce>:<nc>:<cnonce>:<qop>:MD5(<request-method>:<uri>)) string HA1 = DigestUtils.CalculateMD5($"{authObject.Username}:{authObject.Realm}:{Password}"); string HD = $"{authObject.Nonce}:{authObject.Nc}:{authObject.Cnonce}:{authObject.Qop}"; string HA2 = DigestUtils.CalculateMD5($"POST:{authObject.Uri}"); authObject.Response = DigestUtils.CalculateMD5($"{HA1}:{HD}:{HA2}"); // Form1.form.outPut("计算出的 response: " + authObject.Response); return authObject.authorizationString(); } } }
理解了就挺简单的。