diff --git a/AuthStudy.Authentication.Basic/BasicAuthenticationDefaults.cs b/AuthStudy.Authentication.Basic/BasicAuthenticationDefaults.cs index 982b9c5..faebcbb 100644 --- a/AuthStudy.Authentication.Basic/BasicAuthenticationDefaults.cs +++ b/AuthStudy.Authentication.Basic/BasicAuthenticationDefaults.cs @@ -6,7 +6,8 @@ using System.Threading.Tasks; namespace AuthStudy.Authentication.Basic { - internal class BasicAuthenticationDefaults + public class BasicAuthenticationDefaults { + public const string AuthenticationScheme = "BasicScheme"; } } diff --git a/AuthStudy.Authentication.Basic/BasicAuthenticationExtensions.cs b/AuthStudy.Authentication.Basic/BasicAuthenticationExtensions.cs index 541a0fa..ae0d6ac 100644 --- a/AuthStudy.Authentication.Basic/BasicAuthenticationExtensions.cs +++ b/AuthStudy.Authentication.Basic/BasicAuthenticationExtensions.cs @@ -4,9 +4,35 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; + namespace AuthStudy.Authentication.Basic { - internal class BasicAuthenticationExtensions + public static class BasicAuthenticationExtensions { + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder) + { + return builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme); + } + + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme) + { + return builder.AddBasic(authenticationScheme, configureOptions: null); + } + + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action configureOptions) + { + return builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme, configureOptions); + } + + public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action? configureOptions) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddScheme(authenticationScheme, configureOptions); + } } } diff --git a/AuthStudy.Authentication.Basic/BasicAuthenticationHandler.cs b/AuthStudy.Authentication.Basic/BasicAuthenticationHandler.cs index ceed023..5d7b982 100644 --- a/AuthStudy.Authentication.Basic/BasicAuthenticationHandler.cs +++ b/AuthStudy.Authentication.Basic/BasicAuthenticationHandler.cs @@ -2,11 +2,172 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Encodings.Web; using System.Threading.Tasks; +using AuthStudy.Authentication.Basic.Events; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + namespace AuthStudy.Authentication.Basic { - internal class BasicAuthenticationHandler + public class BasicAuthenticationHandler : AuthenticationHandler { + private const string _Scheme = "BasicScheme"; + + private readonly UTF8Encoding _utf8ValidatingEncoding = new UTF8Encoding(false, true); + + public BasicAuthenticationHandler + ( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock + ) + : base(options, logger, encoder, clock) + { + } + + protected new BasicAuthenticationEvents? Events + { + get { return (BasicAuthenticationEvents?)base.Events; } + set { base.Events = value; } + } + + protected override Task CreateEventsAsync() => Task.FromResult(new BasicAuthenticationEvents()); + + protected override async Task HandleAuthenticateAsync() + { + string? authorizationHeader = Request.Headers["Authorization"]; + if (string.IsNullOrEmpty(authorizationHeader)) + { + return AuthenticateResult.NoResult(); + } + + // Exact match on purpose, rather than using string compare + // asp.net request parsing will always trim the header and remove trailing spaces + if (_Scheme == authorizationHeader) + { + const string noCredentialsMessage = "Authorization scheme was Basic but the header had no credentials."; + Logger.LogInformation(noCredentialsMessage); + return AuthenticateResult.Fail(noCredentialsMessage); + } + + if (!authorizationHeader.StartsWith(_Scheme + ' ', StringComparison.OrdinalIgnoreCase)) + { + return AuthenticateResult.NoResult(); + } + + string encodedCredentials = authorizationHeader.Substring(_Scheme.Length).Trim(); + + try + { + string decodedCredentials = string.Empty; + byte[] base64DecodedCredentials; + try + { + base64DecodedCredentials = Convert.FromBase64String(encodedCredentials); + } + catch (FormatException) + { + const string failedToDecodeCredentials = "Cannot convert credentials from Base64."; + Logger.LogInformation(failedToDecodeCredentials); + return AuthenticateResult.Fail(failedToDecodeCredentials); + } + + try + { + decodedCredentials = _utf8ValidatingEncoding.GetString(base64DecodedCredentials); + } + catch (Exception ex) + { + const string failedToDecodeCredentials = "Cannot build credentials from decoded base64 value, exception {ex.Message} encountered."; + Logger.LogInformation(failedToDecodeCredentials, ex.Message); + return AuthenticateResult.Fail(ex.Message); + } + + var delimiterIndex = decodedCredentials.IndexOf(":", StringComparison.OrdinalIgnoreCase); + if (delimiterIndex == -1) + { + const string missingDelimiterMessage = "Invalid credentials, missing delimiter."; + Logger.LogInformation(missingDelimiterMessage); + return AuthenticateResult.Fail(missingDelimiterMessage); + } + + var username = decodedCredentials.Substring(0, delimiterIndex); + var password = decodedCredentials.Substring(delimiterIndex + 1); + + var validateCredentialsContext = new ValidateCredentialsContext(Context, Scheme, Options) + { + Username = username, + Password = password + }; + + if (Events != null) + { + await Events.ValidateCredentials(validateCredentialsContext); + } + + + if (validateCredentialsContext.Result != null && + validateCredentialsContext.Result.Succeeded) + { + var ticket = new AuthenticationTicket(validateCredentialsContext.Principal, Scheme.Name); + return AuthenticateResult.Success(ticket); + } + + if (validateCredentialsContext.Result != null && + validateCredentialsContext.Result.Failure != null) + { + return AuthenticateResult.Fail(validateCredentialsContext.Result.Failure); + } + + return AuthenticateResult.NoResult(); + } + catch (Exception ex) + { + var authenticationFailedContext = new BasicAuthenticationFailedContext(Context, Scheme, Options) + { + Exception = ex + }; + + if (Events != null) + { + await Events.AuthenticationFailed(authenticationFailedContext).ConfigureAwait(true); + } + + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + throw; + } + } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + if (!Request.IsHttps && !Options.AllowInsecureProtocol) + { + const string insecureProtocolMessage = "Request is HTTP, Basic Authentication will not respond."; + Logger.LogInformation(insecureProtocolMessage); + Response.StatusCode = StatusCodes.Status421MisdirectedRequest; + } + else + { + Response.StatusCode = 401; + if (!Options.SuppressWWWAuthenticateHeader) + { + var headerValue = _Scheme + $" realm=\"{Options.Realm}\""; + Response.Headers.Append(HeaderNames.WWWAuthenticate, headerValue); + } + } + + return Task.CompletedTask; + } } } diff --git a/AuthStudy.Authentication.Basic/BasicAuthenticationOptions.cs b/AuthStudy.Authentication.Basic/BasicAuthenticationOptions.cs index 7a7d595..8bb3cc4 100644 --- a/AuthStudy.Authentication.Basic/BasicAuthenticationOptions.cs +++ b/AuthStudy.Authentication.Basic/BasicAuthenticationOptions.cs @@ -1,13 +1,94 @@  +using AuthStudy.Authentication.Basic.Events; + using Microsoft.AspNetCore.Authentication; namespace AuthStudy.Authentication.Basic { public class BasicAuthenticationOptions : AuthenticationSchemeOptions { + private string _realm = string.Empty; + + /// + /// 创建用默认值初始化的选项的实例 + /// public BasicAuthenticationOptions() { + } + + /// + /// 获取或设置在WWW-Authenticate标头中发送的 Realm + /// + /// + /// Realm值(区分大小写)与正在访问的服务器的规范根URL相结合,定义了保护空间。 + /// 这些领域允许将服务器上受保护的资源划分为一组保护空间,每个空间都有自己的身份验证方案和/或授权数据库。 + /// + public string Realm + { + get + { + return _realm; + } + + set + { + if (!string.IsNullOrEmpty(value) && !IsAscii(value)) + { + throw new ArgumentException("Realm must be US ASCII"); + } + + _realm = value; + } + } + + + /// + /// 获取或设置一个标志,该标志指示是否将在未经授权的响应上取消WWW-Authenticate标头 + /// + /// + /// 身份验证方案控制浏览器UI,并允许浏览器以正确的方式进行身份验证,弹出一个允许输入用户名和密码的UI + /// 有些用户可能希望对JavaScript XMLHttpRequest请求抑制这种行为 + /// 将此标志设置为True将抑制WWW-Authenticate标头,从而抑制浏览器登录提示,只需发送401状态代码,您就必须对自己做出反应。 + /// + public bool SuppressWWWAuthenticateHeader + { + get; set; + } + + /// + /// 获取或设置一个标志,该标志指示处理程序是否会提示对HTTP请求进行身份验证 + /// + public bool AllowInsecureProtocol + { + get; set; + } + + /// + /// 事件 + /// + public new BasicAuthenticationEvents? Events + + { + get { return (BasicAuthenticationEvents?)base.Events; } + + set { base.Events = value; } + } + + + /// + /// 判断给定的字符串是否全部是ASCII字符 + /// + private static bool IsAscii(string input) + { + foreach (char c in input) + { + if (c < 32 || c >= 127) + { + return false; + } + } + return true; } } } \ No newline at end of file diff --git a/AuthStudy.Authentication.Basic/Events/BasicAuthenticationEvents.cs b/AuthStudy.Authentication.Basic/Events/BasicAuthenticationEvents.cs new file mode 100644 index 0000000..b55c5f9 --- /dev/null +++ b/AuthStudy.Authentication.Basic/Events/BasicAuthenticationEvents.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AuthStudy.Authentication.Basic.Events +{ + public class BasicAuthenticationEvents + { + public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + public Func OnValidateCredentials { get; set; } = context => Task.CompletedTask; + + public virtual Task AuthenticationFailed(BasicAuthenticationFailedContext context) => OnAuthenticationFailed(context); + + public virtual Task ValidateCredentials(ValidateCredentialsContext context) => OnValidateCredentials(context); + } +} diff --git a/AuthStudy.Authentication.Basic/Events/BasicAuthenticationFailedContext.cs b/AuthStudy.Authentication.Basic/Events/BasicAuthenticationFailedContext.cs new file mode 100644 index 0000000..32e53ff --- /dev/null +++ b/AuthStudy.Authentication.Basic/Events/BasicAuthenticationFailedContext.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace AuthStudy.Authentication.Basic.Events +{ + public class BasicAuthenticationFailedContext : ResultContext + { + public BasicAuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, BasicAuthenticationOptions options) + : base(context, scheme, options) + { + } + + public Exception? Exception { get; set; } + } +} diff --git a/AuthStudy.Authentication.Basic/Events/ValidateCredentialsContext.cs b/AuthStudy.Authentication.Basic/Events/ValidateCredentialsContext.cs new file mode 100644 index 0000000..05e131c --- /dev/null +++ b/AuthStudy.Authentication.Basic/Events/ValidateCredentialsContext.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace AuthStudy.Authentication.Basic.Events +{ + public class ValidateCredentialsContext : ResultContext + { + public ValidateCredentialsContext(HttpContext context, AuthenticationScheme scheme, BasicAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// 用户名 + /// + public string? Username { get; set; } + + /// + /// 密码 + /// + public string? Password { get; set; } + } +} diff --git a/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs b/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs index 157df00..edd093a 100644 --- a/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs +++ b/AuthStudy.Authentication.Browser/BrowserAuthenticationHandler.cs @@ -85,8 +85,8 @@ namespace AuthStudy.Authentication.Browser //声明(身份项) var browser = new Claim("Browser", clientInfo.UA.ToString()); //浏览器 - var os = new Claim("OS", clientInfo.OS.ToString()); //操作系统 - var device = new Claim("Device", clientInfo.Device.ToString()); //设备 //设备 + var os = new Claim("OS", clientInfo.OS.ToString()); //操作系统 + var device = new Claim("Device", clientInfo.Device.ToString()); //设备 //设备 //声明集合 var Claims = new List diff --git a/AuthStudy.Authentication.Digest/AuthStudy.Authentication.Digest.csproj b/AuthStudy.Authentication.Digest/AuthStudy.Authentication.Digest.csproj new file mode 100644 index 0000000..d8b8d08 --- /dev/null +++ b/AuthStudy.Authentication.Digest/AuthStudy.Authentication.Digest.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/AuthStudy.Authentication.Digest/Class1.cs b/AuthStudy.Authentication.Digest/Class1.cs new file mode 100644 index 0000000..66ce604 --- /dev/null +++ b/AuthStudy.Authentication.Digest/Class1.cs @@ -0,0 +1,7 @@ +namespace AuthStudy.Authentication.Digest +{ + public class Class1 + { + + } +} \ No newline at end of file diff --git a/AuthStudy.WebApp/Auth/AuthenticationSchemeList.cs b/AuthStudy.WebApp/Auth/AuthenticationSchemeList.cs index 6e78b32..ee5c528 100644 --- a/AuthStudy.WebApp/Auth/AuthenticationSchemeList.cs +++ b/AuthStudy.WebApp/Auth/AuthenticationSchemeList.cs @@ -16,5 +16,10 @@ /// 基类实现方式 /// public const string BrowserScheme = "BrowserScheme"; + + /// + /// 基本身份认证 + /// + public const string BasicScheme = "BasicScheme"; } } diff --git a/AuthStudy.WebApp/AuthStudy.WebApp.csproj b/AuthStudy.WebApp/AuthStudy.WebApp.csproj index d369b50..9e85913 100644 --- a/AuthStudy.WebApp/AuthStudy.WebApp.csproj +++ b/AuthStudy.WebApp/AuthStudy.WebApp.csproj @@ -14,6 +14,7 @@ + diff --git a/AuthStudy.WebApp/Controllers/AccountsController.cs b/AuthStudy.WebApp/Controllers/AccountsController.cs index 99eb7d6..554314f 100644 --- a/AuthStudy.WebApp/Controllers/AccountsController.cs +++ b/AuthStudy.WebApp/Controllers/AccountsController.cs @@ -16,8 +16,11 @@ namespace AuthStudy.WebApp.Controllers } - //[Authorize(AuthenticationSchemes = BrowserAuthenticationDefault.SchemeName)] - [Authorize(AuthenticationSchemes = "BrowserAuthenticationHandlerByBase")] + //[Authorize(AuthenticationSchemes = AuthenticationSchemeList.BaseBrowserScheme)] + //[Authorize(AuthenticationSchemes = AuthenticationSchemeList.BrowserScheme)] + //[Authorize(AuthenticationSchemes = AuthenticationSchemeList.BasicScheme)] + [Authorize(AuthenticationSchemes = $"{AuthenticationSchemeList.BaseBrowserScheme},{AuthenticationSchemeList.BrowserScheme},{AuthenticationSchemeList.BasicScheme}")] + //[Authorize] [HttpGet] public IActionResult GetAll() { diff --git a/AuthStudy.WebApp/Program.cs b/AuthStudy.WebApp/Program.cs index 0ee7853..4f3e4d4 100644 --- a/AuthStudy.WebApp/Program.cs +++ b/AuthStudy.WebApp/Program.cs @@ -1,4 +1,5 @@ +using AuthStudy.Authentication.Basic; using AuthStudy.Authentication.Browser; namespace AuthStudy.WebApp @@ -27,10 +28,13 @@ namespace AuthStudy.WebApp ); builder.Services .AddAuthentication(AuthenticationSchemeList.BaseBrowserScheme) + //浏览器认证 .AddScheme(AuthenticationSchemeList.BaseBrowserScheme, option => { option.AllowBrowsers = new List() { "Edge", "Chrome", "Firefox" }; - }); + }) + //基本认证 + .AddBasic(AuthenticationSchemeList.BasicScheme); //默认基类实现注册 diff --git a/AuthStudy.sln b/AuthStudy.sln index b331da4..b0e3914 100644 --- a/AuthStudy.sln +++ b/AuthStudy.sln @@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{947159C1-4 Docs\说明.md = Docs\说明.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthStudy.Authentication.Digest", "AuthStudy.Authentication.Digest\AuthStudy.Authentication.Digest.csproj", "{40540D06-49E7-4F39-AE72-66F8A074D86F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +56,10 @@ Global {946FECCE-6382-4138-B840-4189B4F7108A}.Debug|Any CPU.Build.0 = Debug|Any CPU {946FECCE-6382-4138-B840-4189B4F7108A}.Release|Any CPU.ActiveCfg = Release|Any CPU {946FECCE-6382-4138-B840-4189B4F7108A}.Release|Any CPU.Build.0 = Release|Any CPU + {40540D06-49E7-4F39-AE72-66F8A074D86F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40540D06-49E7-4F39-AE72-66F8A074D86F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40540D06-49E7-4F39-AE72-66F8A074D86F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40540D06-49E7-4F39-AE72-66F8A074D86F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -64,6 +70,7 @@ Global {F320DAFE-F490-4F2B-B2C8-F32E71903304} = {0F6CCE40-BD73-4103-99A0-8FE31B4CC7E6} {C9F1FEDE-FEBC-4507-B70B-7A4DC1AD88EB} = {0F6CCE40-BD73-4103-99A0-8FE31B4CC7E6} {946FECCE-6382-4138-B840-4189B4F7108A} = {0F6CCE40-BD73-4103-99A0-8FE31B4CC7E6} + {40540D06-49E7-4F39-AE72-66F8A074D86F} = {0F6CCE40-BD73-4103-99A0-8FE31B4CC7E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93CE07D0-855A-4D3E-B5FF-D9E9D9142536}