本文最后更新于 313 天前,其中的信息可能已经有所发展或是发生改变。
故事是这样的:
前些日子接了一个对接上传接口的活,里面的接口有个叫”注册接口“、”保活接口“……,里面并没有说明密码如何传输。于是我截图问对方,如何写?对方直接甩了一句”百度下摘要认证,国际通用的认证方式“。于是我查资料,恶补。
相关资料:
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();
}
}
}
理解了就挺简单的。