CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
HTTP认证之摘要认证——Digest
0
.NetCore
小笨蛋
发布于:2023年01月15日
更新于:2023年01月15日
194
#custom-toc-container
### 一、概述 > Digest认证是为了修复[基本认证](https://www.codesnippet.cn/home/list/196 "基本认证")协议的严重缺陷而设计的,秉承“绝不通过明文在网络发送密码”的原则,通过“密码摘要”进行认证,大大提高了安全性。 相对于基本认证,主要有如下改进: - 绝不通过明文在网络上发送密码 - 可以有效防止恶意用户进行重放攻击 - 可以有选择的防止对报文内容的篡改 需要注意的是,摘要认证除了能够保护密码之外,并不能保护其他内容,与HTTPS配合使用仍是一个良好的选择。以下是摘要认证的具体流程图: [![](/uploads/images/20230115/183644-6f8b10b1952b4940be558e4ab9f36fc2.jpg)](https://www.codesnippet.cn) 看到上面出现了那么多之前没见过的参数,是不是有点慌(或是兴奋)?别着急,这里先给出一个概览: - [WWW-Authentication](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/WWW-Authenticate "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,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了 ### 二、剖析 1.当打开需要认证的页面时,会弹出一个对话框,要求用户输入用户名和密码 [![](/uploads/images/20230115/183759-8378692d1e0e4ba0ada00884944e80ad.jpg)](https://www.codesnippet.cn) 2.使用Fidder监听请求,可以看到在未进行认证或认证失败的情况下,服务端会返回401 Unauthorized给客户端,并附带Challenge [![](/uploads/images/20230115/183928-e69fdeb54a334ec9bce6eebdb9c18c6d.jpg)](https://www.codesnippet.cn) 3.输入正确的用户名和密码后,浏览器会生成密码摘要以及其他信息发送给服务端,服务端认证成功后,返回一些与授权会话相关的附加信息,放在Authorization-Info中。 其中,客户端选择的保护质量策略为auth,response就是通过计算得到的密码摘要,具体计算方式如下(使用默认的MD5加密算法):`MD5(MD5(A1):
:
:
:
:MD5(A2))` | 算法 | A1 | | ----------- | --------------------------------------------------- | | MD5(默认) | `
:
:
` | | MD5-sess | `MD5(
:
:
):
:
` | | qop | A2 | | ------------ | ------------------------------------------------- | | auth(默认) | `
:
` | | auth-int | `
:
:MD5(
)` | 另外,rspauth使得客户端可以对服务器进行认证,称为响应摘要。响应摘要的计算与请求摘要类似,但由于响应中没有方法,而且报文实体数据有所不同,所有只有报文主题信息A2不同。具体区别如下: | qop | A2 | | ------------ | ---------------------------------- | | auth(默认) | `:
` | | auth-int | `:
:MD5(
)` | [![](/uploads/images/20230115/184245-e4bdc9d779984605b746020297dc3932.jpg)](https://www.codesnippet.cn) 4.当服务端随机数过期时,再次请求认证,可以看到质询中增加了stale=true,用户无需再次输入用户名和密码,浏览器会自动使用新的质询参数进行密码摘要的计算。 [![](/uploads/images/20230115/184359-d42d068424c14ddda523e2fa1e29a7b4.jpg)](https://www.codesnippet.cn) ### 三、注意事项 1.预授权:服务端预先告知客户端下一个随机数是多少,使得客户端可以直接生成正确的Authorization首部,避免了多次“请求/质询”。常用的有一下三种方式: - 服务器预先在Authorization-Info成功首部中发送下一个随机数nextnonce。虽然这种机制加快了事务处理的速度,但是它也破坏了对同一台服务器的多次请求进行管道化的功能,可能会造成很大的损失。 - 服务器允许在一小段时间内使用同一个随机数。这也就是我们上面剖析中使用的机制,在一定时间内使用同一个随机数或限制某个随机数的重用次数,当过期时,声明stale=true。虽然这确实降低了安全性,但是重用的随机数的生存周期是可控的,应该在安全和性能之间找到平衡。 - 客户端和服务器使用同步的、可预测的随机数生成算法。 2.RFC 2617建议采用这个假想的随机数公式:BASE64(timestamp MD5(timestamp ":" ETag ":" private-key)) 其中,timestamp是服务器产生随机数的时间或其他不重复的值,ETag是与所请求实体有关的HTTP ETag首部的值,private-key是只有服务器知道的私钥。 > 接下来就赶紧通过代码来实践一下,以下教程使用默认的MD5摘要算法、auth策略,基于ASP.NET Core WebApi框架。如有兴趣,可[查看源码](https://gitee.com/homezzm/DotNetStackLibraries/tree/master/DotNetStackLibraries/AspNetCore.WebApi.Authentication.Digest "查看源码") ### 四、准备工作 在开始之前,先把最基本的业务逻辑准备好,只有一个根据用户名获取密码的方法: ```csharp public class UserService { public static string GetPassword(string userName) => userName; } ``` 还有MD5加密的一些扩展方法 ```csharp public static class MD5HashExtensions { public static string ToMD5Hash(this string input) => MD5Helper.Encrypt(input); } public class MD5Helper { public static string Encrypt(string plainText) => Encrypt(plainText, Encoding.UTF8); public static string Encrypt(string plainText, Encoding encoding) { var bytes = encoding.GetBytes(plainText); return Encrypt(bytes); } public static string Encrypt(byte[] bytes) { using (var md5 = MD5.Create()) { var hash = md5.ComputeHash(bytes); return FromHash(hash); } } private static string FromHash(byte[] hash) { var sb = new StringBuilder(); foreach (var t in hash) { sb.Append(t.ToString("x2")); } return sb.ToString(); } } ``` ### 五、编码 > 以下代码书写在自定义授权过滤器中,继承自Attribute, IAuthorizationFilter 1.首先,先确定使用的认证方案为Digest,并指定Realm,设置Qop的策略为auth,这里我们采用的预处理方式为在一定时间段内可以重用nonce,指定过期时间为10s ```csharp public const string AuthenticationScheme = "Digest"; public const string AuthenticationRealm = "http://localhost:32435"; public const string Qop = "auth"; //设置 nonce 过期时间为10s public const int MaxNonceAgeSeconds = 10; ``` 2.接着,我们再把常用的常量封装一下 ```csharp public static class AuthenticateHeaderNames { public const string UserName = "username"; public const string Realm = "realm"; public const string Nonce = "nonce"; public const string ClientNonce = "cnonce"; public const string NonceCounter = "nc"; public const string Qop = "qop"; public const string Response = "response"; public const string Uri = "uri"; public const string RspAuth = "rspauth"; public const string Stale = "stale"; } public static class QopValues { public const string Auth = "auth"; public const string AuthInt = "auth-int"; } ``` 3.在没有进行认证或认证失败时,服务端需要返回401 Unauthorized,并对客户端发出质询,一下是质询需要包含的内容(“stale”参数指示nonce是否过期) ```csharp private void AddChallenge(HttpResponse response, bool stale) { var partList = new List
>() { (AuthenticateHeaderNames.Realm, AuthenticationRealm, true), (AuthenticateHeaderNames.Qop, Qop, true), (AuthenticateHeaderNames.Nonce, GetNonce(), true), }; var value = $"{AuthenticationScheme} {string.Join(", ", partList.Select(part => FormatHeaderPart(part)))}"; if (stale) { value += $", {FormatHeaderPart((AuthenticateHeaderNames.Stale, "true", false))}"; } response.Headers.Append(HeaderNames.WWWAuthenticate, value); } private string GetNonce(DateTimeOffset? timestamp = null) { var privateKey = "test private key"; var timestampStr = timestamp?.ToString() ?? DateTimeOffset.UtcNow.ToString(); return Convert.ToBase64String(_encoding.GetBytes($"{ timestampStr } {$"{timestampStr} : {privateKey}".ToMD5Hash()}")); } private string FormatHeaderPart((string Name, string Value, bool ShouldQuote) part) => part.ShouldQuote ? $"{part.Name}=\"{part.Value}\"" : $"{part.Name}={part.Value}"; ``` 4.客户端请求认证后,服务端需要使用HTTP Request中Authorization标头的参数进行摘要计算,所以我们需要将这些参数解析出来并封装成一个类对象AuthorizationHeader。 ```csharp private AuthorizationHeader GetAuthenticationHeader(HttpRequest request) { try { var credentials = GetCredentials(request); if (credentials != null) { var authorizationHeader = new AuthorizationHeader() { RequestMethod = request.Method, }; var nameValueStrs = credentials.Replace("\"", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()); foreach (var nameValueStr in nameValueStrs) { var index = nameValueStr.IndexOf('='); var name = nameValueStr.Substring(0, index); var value = nameValueStr.Substring(index + 1); switch (name) { case AuthenticateHeaderNames.UserName: authorizationHeader.UserName = value; break; case AuthenticateHeaderNames.Realm: authorizationHeader.Realm = value; break; case AuthenticateHeaderNames.Nonce: authorizationHeader.Nonce = value; break; case AuthenticateHeaderNames.ClientNonce: authorizationHeader.ClientNonce = value; break; case AuthenticateHeaderNames.NonceCounter: authorizationHeader.NonceCounter = value; break; case AuthenticateHeaderNames.Qop: authorizationHeader.Qop = value; break; case AuthenticateHeaderNames.Response: authorizationHeader.Response = value; break; case AuthenticateHeaderNames.Uri: authorizationHeader.Uri = value; break; } } return authorizationHeader; } } catch { } return null; } private string GetCredentials(HttpRequest request) { string credentials = null; string authorization = request.Headers[HeaderNames.Authorization]; //请求中存在 Authorization 标头且认证方式为 Digest if (authorization?.StartsWith(AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == true) { credentials = authorization.Substring(AuthenticationScheme.Length).Trim(); } return credentials; } public class AuthorizationHeader { public string UserName { get; set; } public string Realm { get; set; } public string Nonce { get; set; } public string ClientNonce { get; set; } public string NonceCounter { get; set; } public string Qop { get; set; } public string Response { get; set; } public string RequestMethod { get; set; } public string Uri { get; set; } } ``` 5.进行摘要计算的参数信息已经齐备了,不过别着急,先来校验一下nonce的有效性。 ```csharp ///
/// 验证Nonce是否有效 ///
///
///
true:验证通过;false:验证失败;null:随机数过期
private bool? ValidateNonce(string nonce) { try { var plainNonce = _encoding.GetString(Convert.FromBase64String(nonce)); var timestamp = DateTimeOffset.Parse(plainNonce.Substring(0, plainNonce.LastIndexOf(' '))); //验证Nonce是否被篡改 var isValid = nonce == GetNonce(timestamp); //验证是否过期 if (Math.Abs((timestamp - DateTimeOffset.UtcNow).TotalSeconds) > MaxNonceAgeSeconds) { return isValid ? (bool?)null : false; } return isValid; } catch { return false; } } ``` 6.好,接下来就来进行摘要计算吧,其实就是套用公式,如果不记得了,可以重温一下第一节。 ```csharp private static string GetComputedResponse(AuthorizationHeader authorizationHeader, string password) { var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash(); var a2Hash = $"{authorizationHeader.RequestMethod}:{authorizationHeader.Uri}".ToMD5Hash(); return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash(); } ``` 7.如果认证通过,我们通过Authorization-Info返回一些授权会话的信息。 ```csharp private void AddAuthorizationInfo(HttpResponse response, AuthorizationHeader authorizationHeader, string password) { var partList = new List
>() { (AuthenticateHeaderNames.Qop, authorizationHeader.Qop, true), (AuthenticateHeaderNames.RspAuth, GetRspAuth(authorizationHeader, password), true), (AuthenticateHeaderNames.ClientNonce, authorizationHeader.ClientNonce, true), (AuthenticateHeaderNames.NonceCounter, authorizationHeader.NonceCounter, false) }; response.Headers.Append("Authorization-Info", string.Join(", ", partList.Select(part => FormatHeaderPart(part)))); } private string GetRspAuth(AuthorizationHeader authorizationHeader, string password) { var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash(); var a2Hash = $":{authorizationHeader.Uri}".ToMD5Hash(); return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash(); } ``` 8.我们把整个认证流程整理一下 ```csharp public void OnAuthorization(AuthorizationFilterContext context) { //请求允许匿名访问 if (context.Filters.Any(item => item is IAllowAnonymousFilter)) return; var authorizationHeader = GetAuthenticationHeader(context.HttpContext.Request); var stale = false; if(authorizationHeader != null) { var isValid = ValidateNonce(authorizationHeader.Nonce); //随机数过期 if(isValid == null) { stale = true; } else if(isValid == true) { var password = UserService.GetPassword(authorizationHeader.UserName); string computedResponse = null; switch (authorizationHeader.Qop) { case QopValues.Auth: computedResponse = GetComputedResponse(authorizationHeader, password); break; default: context.Result = new BadRequestObjectResult($"qop指定策略必须为\"{QopValues.Auth}\""); break; } if (computedResponse == authorizationHeader.Response) { AddAuthorizationInfo(context.HttpContext.Response, authorizationHeader, password); return; } } } context.Result = new UnauthorizedResult(); AddChallenge(context.HttpContext.Response, stale); } ``` 9.最后,在需要认证的Action上加上自定义过滤器特性,大功告成!自己测试一下吧! ### 六、封装为中间件 > 照例,接下来我们将摘要认证封装为[ASP.NET Core](https://docs.microsoft.com/zh-cn/aspnet/?view=aspnetcore-2.2#pivot=core "ASP.NET Core")中间件,便于使用和扩展。以下封装采用[Jwt Bearer](https://github.com/aspnet/AspNetCore/tree/master/src/Security/Authentication/JwtBearer/ "Jwt Bearer")封装规范。以下代码较长,推荐直接去看源码。 1.首先封装常量(之前提到过的就不说了) ```csharp public static class DigestDefaults { public const string AuthenticationScheme = "Digest"; } ``` 2.然后封装Basic认证的Options,包括Realm、Qop、Private key和事件,继承自Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions。在事件内部,我们定义了获取密码行为和质询行为,分别用来根据用户名获取密码和在HTTP Response中添加质询信息。要注意的是,获取密码行为要求必须由用户实现,毕竟我们内部是不知道密码的。 ```csharp public class DigestOptions : AuthenticationSchemeOptions { public const string DefaultQop = QopValues.Auth; public const int DefaultMaxNonceAgeSeconds = 10; public string Realm { get; set; } public string Qop { get; set; } = DefaultQop; public int MaxNonceAgeSeconds { get; set; } = DefaultMaxNonceAgeSeconds; public string PrivateKey { get; set; } public new DigestEvents Events { get => (DigestEvents)base.Events; set => base.Events = value; } } public class DigestEvents { public DigestEvents(Func
> onGetPassword) { OnGetPassword = onGetPassword; } public Func
> OnGetPassword { get; set; } = context => throw new NotImplementedException($"{nameof(OnGetPassword)} must be implemented!"); public Func
OnChallenge { get; set; } = context => Task.CompletedTask; public virtual Task
GetPassword(GetPasswordContext context) => OnGetPassword(context); public virtual Task Challenge(DigestChallengeContext context) => OnChallenge(context); } public class GetPasswordContext : ResultContext
{ public GetPasswordContext( HttpContext context, AuthenticationScheme scheme, DigestOptions options) : base(context, scheme, options) { } public string UserName { get; set; } } public class DigestChallengeContext : PropertiesContext
{ public DigestChallengeContext( HttpContext context, AuthenticationScheme scheme, DigestOptions options, AuthenticationProperties properties) : base(context, scheme, options, properties) { } ///
/// 在认证期间出现的异常 ///
public Exception AuthenticateFailure { get; set; } public bool Stale { get; set; } ///
/// 指定是否已被处理,如果已处理,则跳过默认认证逻辑 ///
public bool Handled { get; private set; } ///
/// 跳过默认认证逻辑 ///
public void HandleResponse() => Handled = true; } ``` 3.接下来,就是对认证过程处理的封装了,需要继承自Microsoft.AspNetCore.Authentication.AuthenticationHandler ```csharp public class DigestHandler : AuthenticationHandler
{ private static readonly Encoding _encoding = Encoding.UTF8; public DigestHandler( IOptionsMonitor
options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected new DigestEvents Events { get => (DigestEvents)base.Events; set => base.Events = value; } ///
/// 确保创建的 Event 类型是 DigestEvents ///
///
protected override Task
CreateEventsAsync() => throw new NotImplementedException($"{nameof(Events)} must be created"); protected async override Task
HandleAuthenticateAsync() { var authorizationHeader = GetAuthenticationHeader(Context.Request); if (authorizationHeader == null) { return AuthenticateResult.NoResult(); } try { var isValid = ValidateNonce(authorizationHeader.Nonce); //随机数过期 if (isValid == null) { var properties = new AuthenticationProperties(); properties.SetParameter(AuthenticationHeaderNames.Stale, true); return AuthenticateResult.Fail(string.Empty, properties); } else if (isValid == true) { var getPasswordContext = new GetPasswordContext(Context, Scheme, Options) { UserName = authorizationHeader.UserName }; var password = await Events.GetPassword(getPasswordContext); string computedResponse = null; switch (authorizationHeader.Qop) { case QopValues.Auth: computedResponse = GetComputedResponse(authorizationHeader, password); break; default: return AuthenticateResult.Fail($"qop指定策略必须为\"{QopValues.Auth}\""); } if (computedResponse == authorizationHeader.Response) { var claim = new Claim(ClaimTypes.Name, getPasswordContext.UserName); var identity = new ClaimsIdentity(DigestDefaults.AuthenticationScheme); identity.AddClaim(claim); var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name); AddAuthorizationInfo(Context.Response, authorizationHeader, password); return AuthenticateResult.Success(ticket); } } return AuthenticateResult.NoResult(); } catch (Exception ex) { return AuthenticateResult.Fail(ex.Message); } } protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { var authResult = await HandleAuthenticateOnceSafeAsync(); var challengeContext = new DigestChallengeContext(Context, Scheme, Options, properties) { AuthenticateFailure = authResult.Failure, Stale = authResult.Properties?.GetParameter
(AuthenticationHeaderNames.Stale) ?? false }; await Events.Challenge(challengeContext); //质询已处理 if (challengeContext.Handled) return; var challengeValue = GetChallengeValue(challengeContext.Stale); var error = challengeContext.AuthenticateFailure?.Message; if (!string.IsNullOrWhiteSpace(error)) { //将错误信息封装到内部 challengeValue += $", error=\"{ error }\""; } Response.StatusCode = (int)HttpStatusCode.Unauthorized; Response.Headers.Append(HeaderNames.WWWAuthenticate, challengeValue); } private AuthorizationHeader GetAuthenticationHeader(HttpRequest request) { try { var credentials = GetCredentials(request); if (credentials != null) { var authorizationHeader = new AuthorizationHeader() { RequestMethod = request.Method, }; var nameValueStrs = credentials.Replace("\"", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()); foreach (var nameValueStr in nameValueStrs) { var index = nameValueStr.IndexOf('='); var name = nameValueStr.Substring(0, index); var value = nameValueStr.Substring(index + 1); switch (name) { case AuthenticationHeaderNames.UserName: authorizationHeader.UserName = value; break; case AuthenticationHeaderNames.Realm: authorizationHeader.Realm = value; break; case AuthenticationHeaderNames.Nonce: authorizationHeader.Nonce = value; break; case AuthenticationHeaderNames.ClientNonce: authorizationHeader.ClientNonce = value; break; case AuthenticationHeaderNames.NonceCounter: authorizationHeader.NonceCounter = value; break; case AuthenticationHeaderNames.Qop: authorizationHeader.Qop = value; break; case AuthenticationHeaderNames.Response: authorizationHeader.Response = value; break; case AuthenticationHeaderNames.Uri: authorizationHeader.Uri = value; break; } } return authorizationHeader; } } catch { } return null; } private string GetCredentials(HttpRequest request) { string credentials = null; string authorization = request.Headers[HeaderNames.Authorization]; //请求中存在 Authorization 标头且认证方式为 Digest if (authorization?.StartsWith(DigestDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == true) { credentials = authorization.Substring(DigestDefaults.AuthenticationScheme.Length).Trim(); } return credentials; } ///
/// 验证Nonce是否有效 ///
///
///
true:验证通过;false:验证失败;null:随机数过期
private bool? ValidateNonce(string nonce) { try { var plainNonce = _encoding.GetString(Convert.FromBase64String(nonce)); var timestamp = DateTimeOffset.Parse(plainNonce.Substring(0, plainNonce.LastIndexOf(' '))); //验证Nonce是否被篡改 var isValid = nonce == GetNonce(timestamp); //验证是否过期 if (Math.Abs((timestamp - DateTimeOffset.UtcNow).TotalSeconds) > Options.MaxNonceAgeSeconds) { return isValid ? (bool?)null : false; } return isValid; } catch { return false; } } private static string GetComputedResponse(AuthorizationHeader authorizationHeader, string password) { var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash(); var a2Hash = $"{authorizationHeader.RequestMethod}:{authorizationHeader.Uri}".ToMD5Hash(); return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash(); } private void AddAuthorizationInfo(HttpResponse response, AuthorizationHeader authorizationHeader, string password) { var partList = new List
>() { (AuthenticationHeaderNames.Qop, authorizationHeader.Qop, true), (AuthenticationHeaderNames.RspAuth, GetRspAuth(authorizationHeader, password), true), (AuthenticationHeaderNames.ClientNonce, authorizationHeader.ClientNonce, true), (AuthenticationHeaderNames.NonceCounter, authorizationHeader.NonceCounter, false) }; response.Headers.Append("Authorization-Info", string.Join(", ", partList.Select(part => FormatHeaderPart(part)))); } private string GetChallengeValue(bool stale) { var partList = new List
>() { (AuthenticationHeaderNames.Realm, Options.Realm, true), (AuthenticationHeaderNames.Qop, Options.Qop, true), (AuthenticationHeaderNames.Nonce, GetNonce(), true), }; var value = $"{DigestDefaults.AuthenticationScheme} {string.Join(", ", partList.Select(part => FormatHeaderPart(part)))}"; if (stale) { value += $", {FormatHeaderPart((AuthenticationHeaderNames.Stale, "true", false))}"; } return value; } private string GetRspAuth(AuthorizationHeader authorizationHeader, string password) { var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash(); var a2Hash = $":{authorizationHeader.Uri}".ToMD5Hash(); return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash(); } private string GetNonce(DateTimeOffset? timestamp = null) { var privateKey = Options.PrivateKey; var timestampStr = timestamp?.ToString() ?? DateTimeOffset.UtcNow.ToString(); return Convert.ToBase64String(_encoding.GetBytes($"{ timestampStr } {$"{timestampStr} : {privateKey}".ToMD5Hash()}")); } private string FormatHeaderPart((string Name, string Value, bool ShouldQuote) part) => part.ShouldQuote ? $"{part.Name}=\"{part.Value}\"" : $"{part.Name}={part.Value}"; ``` 4.最后,就是要把封装的接口暴露给用户了。 ```csharp public static class DigestExtensions { public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder) => builder.AddDigest(DigestDefaults.AuthenticationScheme, _ => { }); public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder, Action
configureOptions) => builder.AddDigest(DigestDefaults.AuthenticationScheme, configureOptions); public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder, string authenticationScheme, Action
configureOptions) => builder.AddDigest(authenticationScheme, displayName: null, configureOptions: configureOptions); public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action
configureOptions) => builder.AddScheme
(authenticationScheme, displayName, configureOptions); } ``` 5.Digest认证库已经封装好了,我们创建一个ASP.NET Core WebApi程序来测试一下吧。 ```csharp //在 ConfigureServices 中配置认证中间件 public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(DigestDefaults.AuthenticationScheme) .AddDigest(options => { options.Realm = "http://localhost:44550"; options.PrivateKey = "test private key"; options.Events = new DigestEvents(context => Task.FromResult(context.UserName)); }); } //在 Configure 中启用认证中间件 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); } ``` 最后,一定要记得为需要认证的Action添加[Authorize]特性,否则前面做的一切都是徒劳+_+ [查看源码](https://gitee.com/homezzm/DotNetStackLibraries/tree/master/DotNetStackLibraries/AspNetCore.WebApi.Authentication.Digest "查看源码")
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024