这篇文章以实现一个Basic认证来了解下在 .NET CORE 下面如何去实现认证。
首先可以肯定的是认证实现是基于 Middlerware 管道实现的,官方源码地址:https://github.com/aspnet/Security。可以看到官方已经实现了jwt、oauth、google等诸多第三方认证,其原理今天我们就不在这里介绍。
下面我们来实现Basic认证。
AuthenticationSchemeOptions。 负责初始化参数配置。这里我们额外需要一个验证用户的委托。代码如下:
public class BasicOption : AuthenticationSchemeOptions { public BasicOption() : base() { Events = new BasicEvents(); } public Func<string, string, bool> ValidateUser { get; set; } public new BasicEvents Events { get { return (BasicEvents)base.Events; } set { base.Events = value; } } }
BasicDefault。定义一些基本常量
public static class BasicDefault { public const string AuthenticationScheme = "Basic"; public const string DisplayName = "Basic"; }
ResultContext<T>。用于认证流程中上下文扩展
public class BasicTokenValidatedContext : ResultContext<BasicOption> { public BasicTokenValidatedContext(HttpContext context, AuthenticationScheme scheme, BasicOption options) : base(context, scheme, options) { } }
BasicEvents。用于认证流程中各类自定义事件触发,在这里我们定义了一个 验证成功后事件,用于客户端自定义设置
public class BasicEvents { public Func<BasicTokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.CompletedTask; public virtual Task TokenValidated(BasicTokenValidatedContext context) => OnTokenValidated(context); }
AuthenticationHandler<T>。这里认证流程中的核心部分,HandleAuthenticateAsync 用于处理认证。HandleChallengeAsync 用于处理认证失败后续Challenge
public class BasicHandler : AuthenticationHandler<BasicOption> { private const string KEY_AUTHORIZATION = "authorization"; private const string KEY_SPLIT = ":"; protected new BasicEvents Events { get => (BasicEvents)base.Events; set => base.Events = value; } public BasicHandler(IOptionsMonitor<BasicOption> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { string authorization = Request.Headers[KEY_AUTHORIZATION]; if (string.IsNullOrEmpty(authorization)) { Logger.LogInformation("请求头authorization为空,目标路径{0}", Request.Path); return AuthenticateResult.NoResult(); } string token = string.Empty; if (authorization.StartsWith(BasicDefault.AuthenticationScheme + " ", StringComparison.CurrentCultureIgnoreCase)) { token = authorization.Substring(BasicDefault.AuthenticationScheme.Length).Trim(); } if (string.IsNullOrEmpty(token)) { Logger.LogInformation("无效的请求头authorization,目标路径{0}", Request.Path); return AuthenticateResult.NoResult(); } var checkUser = Options.ValidateUser; if (checkUser == null) { Logger.LogInformation("Basic TokenValidator不能,目标路径{0}", Request.Path); return await Task.FromResult(AuthenticateResult.NoResult()); } try { var data = Encoding.UTF8.GetString(Convert.FromBase64String(token)); if (string.IsNullOrEmpty(data)) throw new Exception("basic token 格式错误"); string[] array = data.Split(KEY_SPLIT.ToCharArray()); if (array.Length != 2) throw new Exception("basic token 格式错误"); var username = array[0]; var password = array[1]; if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) throw new Exception("basic token 格式错误"); if (!checkUser(username, password)) { Logger.LogInformation("token 验证失败"); return AuthenticateResult.Fail("token 验证失败"); } var claims = new List<Claim>() { new Claim(ClaimTypes.Name, username) }; var principer = new ClaimsPrincipal(new ClaimsIdentity(claims, BasicDefault.AuthenticationScheme)); var validatedContext = new BasicTokenValidatedContext(Context, Scheme, Options) { Principal = principer }; await Events.TokenValidated(validatedContext); validatedContext.Success(); return validatedContext.Result; } catch (Exception ex) { Logger.LogDebug(token + " validate failed: " + ex.Message); return AuthenticateResult.Fail(ex.Message); } } protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { var authResult = await HandleAuthenticateOnceSafeAsync(); Response.Headers.Add(HeaderNames.WWWAuthenticate, BasicDefault.AuthenticationScheme); Response.StatusCode = 401; if (authResult.Failure != null && !string.IsNullOrEmpty(authResult.Failure.Message)) { var byteMsg = System.Text.Encoding.Default.GetBytes(authResult.Failure.Message); Response.Body.Write(byteMsg, 0, byteMsg.Length); } await base.HandleChallengeAsync(properties); } }
BasicExtensions。用于提供注册到.NET CORE的方法。
public static class BasicExtensions { public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder) => builder.AddBasic(BasicDefault.AuthenticationScheme, _ => { }); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action<BasicOption> configureOptions) => builder.AddBasic(BasicDefault.AuthenticationScheme, configureOptions); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action<BasicOption> configureOptions) => builder.AddBasic(authenticationScheme, displayName: BasicDefault.DisplayName, configureOptions: configureOptions); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<BasicOption> configureOptions) { return builder.AddScheme<BasicOption, BasicHandler>(authenticationScheme, displayName, configureOptions); } }
以上就是完成Basic认证所有的方法。我们发现居然没有涉及到 任何Middlerware的部分。实际原因是 官方实现了默认的 Authentication,里面有一个 IAuthenticationRequestHandler 的集合,我们创建的 AuthenticationHandler<T> 扩展就会加入该集合中,Authentication 会负责对集合中的 每一个 Handler 进行处理。源码部分如下:
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync()) { var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler; if (handler != null && await handler.HandleRequestAsync()) { return; } }
最后我们将Basic认证注册到.NET CORE中
services.AddAuthentication(BasicDefault.AuthenticationScheme) .AddBasic(options => { options.ValidateUser = (username, password) => { var clients = Configuration.GetSection("Clients").Get<List<ClientOptions>>(); if (clients == null || clients.Count == 0) return false; return clients.Exists(x => string.Equals(x.Appkey, username, StringComparison.CurrentCultureIgnoreCase) && string.Equals(x.Appsecret, password, StringComparison.CurrentCultureIgnoreCase)); }; //options.Events = new AspNetCore.Authentication.Basic.Events.BasicEvents(); options.Events.OnTokenValidated = context => { if (context.Principal.Identity.IsAuthenticated) { var clients = Configuration.GetSection("Clients").Get<List<ClientOptions>>(); if (clients == null || clients.Count == 0) return Task.CompletedTask; var appkey = context.Principal.Identity.Name; var actions = clients.Single(x => string.Equals(x.Appkey, appkey, StringComparison.CurrentCultureIgnoreCase)).Actions; context.Properties.SetParameter("actions", actions); } return Task.CompletedTask; }; });
别忘了 在 Configure 方法中加入
app.UseAuthentication();
好了,我们的Basic认证完成了~~
后续问题,测试过程中发现 即使认证不通过的话 action 也能正常访问,需要配合 Authorize 才能触发 Challenge。这里我对Authentication和Authorization又增加了一点疑惑,按道理 前者负责确认 用户,后者负责确认 用户权限,但如果用户确认为非法的情况下,为什么还要等到Authorization这块来处理??