在 HTTP 认证之基本认证 --Basic(一)中介绍了 Basic 认证的工作原理和流程, 接下来就赶紧通过代码来实践一下, 以下教程基于 ASP.NET Core WebApi 框架. 如有兴趣, 可查看源码
一, 准备工作
在开始之前, 先把最基本的用户名密码校验逻辑准备好, 只有一个认证方法:
- public class UserService
- {
- public static User Authenticate(string userName, string password)
- {
- // 用户名, 密码不为空且相等时认证成功
- if (!string.IsNullOrEmpty(userName)
- && !string.IsNullOrEmpty(password)
- && userName == password)
- {
- return new User()
- {
- UserName = userName,
- Password = password
- };
- }
- return null;
- }
- }
- public class User
- {
- public string UserName { get; set; }
- public string Password { get; set; }
- }
二, 编码
1. 首先, 先确定使用的认证方案为 Basic, 并提供默认的的 Realm,
- public const string AuthenticationScheme = "Basic";
- public const string AuthenticationRealm = "Test Realm";
2. 然后, 解析 HTTP Request 获取到 Authorization 标头
- private string GetCredentials(HttpRequest request)
- {
- string credentials = null;
- string authorization = request.Headers[HeaderNames.Authorization];
- // 请求中存在 Authorization 标头且认证方式为 Basic
- if (authorization?.StartsWith(AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == true)
- {
- credentials = authorization.Substring(AuthenticationScheme.Length).Trim();
- }
- return credentials;
- }
3. 接着通过 Base64 逆向解码, 得到要认证的用户名和密码. 如果认证失败, 则返回 401 Unauthorized(不推荐返回 403 Forbidden, 因为这会导致用户在不刷新页面的情况下无法重新尝试认证); 如果认证成功, 继续处理请求.
- public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter
- {
- public void OnAuthorization(AuthorizationFilterContext context)
- {
- // 请求允许匿名访问
- if (context.Filters.Any(item => item is IAllowAnonymousFilter)) return;
- var credentials = GetCredentials(context.HttpContext.Request);
- // 已获取到凭证
- if(credentials != null)
- {
- try
- {
- //Base64 逆向解码得到用户名和密码
- credentials = Encoding.UTF8.GetString(Convert.FromBase64String(credentials));
- var data = credentials.Split(':');
- if (data.Length == 2)
- {
- var userName = data[0];
- var password = data[1];
- var user = UserService.Authenticate(userName, password);
- // 认证成功
- if (user != null) return;
- }
- }
- catch { }
- }
- // 认证失败返回 401
- context.Result = new UnauthorizedResult();
- // 添加质询
- AddChallenge(context.HttpContext.Response);
- }
- private void AddChallenge(HttpResponse response)
- => response.Headers.Append(HeaderNames.WWWAuthenticate, $"{ AuthenticationScheme } Realm={ AuthenticationRealm }");
- }
4. 最后, 在需要认证的 Action 上加上过滤器[AuthorizationFilter], 大功告成! 自己测试一下吧
三, 封装为中间件
ASP.NET Core 相比 ASP.NET 最大的突破大概就是插件配置化了 -- 通过将各个功能封装成中间件, 应用的设计思想配置到应用程序中. 以下封装采用 Jwt Bearer 封装规范.
首先封装常量
- public static class BasicDefaults
- {
- public const string AuthenticationScheme = "Basic";
- }
2. 然后封装 Basic 认证的 Options, 包括 Realm 和事件, 继承自 Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions. 在事件内部, 我们定义了认证行为和质询行为, 分别用来校验认证是否通过和在 HTTP Response 中添加质询信息. 我们将认证逻辑封装成一个委托, 与认证行为独立开来, 方便用户使用委托自定义认证规则.
- public class BasicOptions : AuthenticationSchemeOptions
- {
- public string Realm { get; set; }
- public new BasicEvents Events
- {
- get => (BasicEvents)base.Events;
- set => base.Events = value;
- }
- }
- public class BasicEvents
- {
- public Func<ValidateCredentialsContext, Task> OnValidateCredentials { get; set; } = context => Task.CompletedTask;
- public Func<BasicChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask;
- public virtual Task ValidateCredentials(ValidateCredentialsContext context) => OnValidateCredentials(context);
- public virtual Task Challenge(BasicChallengeContext context) => OnChallenge(context);
- }
- /// <summary>
- /// 封装认证参数信息上下文
- /// </summary>
- public class ValidateCredentialsContext : ResultContext<BasicAuthenticationOptions>
- {
- public ValidateCredentialsContext(HttpContext context, AuthenticationScheme scheme, BasicAuthenticationOptions options) : base(context, scheme, options)
- {
- }
- public string UserName { get; set; }
- public string Password { get; set; }
- }
- public class BasicChallengeContext : PropertiesContext<BasicOptions>
- {
- public BasicChallengeContext(
- HttpContext context,
- AuthenticationScheme scheme,
- BasicOptions options,
- AuthenticationProperties properties)
- : base(context, scheme, options, properties)
- {
- }
- /// <summary>
- /// 在认证期间出现的异常
- /// </summary>
- public Exception AuthenticateFailure { get; set; }
- /// <summary>
- /// 指定是否已被处理, 如果已处理, 则跳过默认认证逻辑
- /// </summary>
- public bool Handled { get; private set; }
- /// <summary>
- /// 跳过默认认证逻辑
- /// </summary>
- public void HandleResponse() => Handled = true;
- }
3. 接下来, 就是对认证过程处理的封装了, 需要继承自 Microsoft.AspNetCore.Authentication.AuthenticationHandler
- public class BasicHandler : AuthenticationHandler<BasicOptions>
- {
- public BasicHandler(IOptionsMonitor<BasicOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
- {
- }
- protected new BasicEvents Events
- {
- get => (BasicEvents)base.Events;
- set => base.Events = value;
- }
- /// <summary>
- /// 确保创建的 Event 类型是 BasicEvents
- /// </summary>
- /// <returns></returns>
- protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new BasicEvents());
- protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
- {
- var credentials = GetCredentials(Request);
- if(credentials == null)
- {
- return AuthenticateResult.NoResult();
- }
- try
- {
- credentials = Encoding.UTF8.GetString(Convert.FromBase64String(credentials));
- var data = credentials.Split(':');
- if(data.Length != 2)
- {
- return AuthenticateResult.Fail("Invalid credentials, error format.");
- }
- var validateCredentialsContext = new ValidateCredentialsContext(Context, Scheme, Options)
- {
- UserName = data[0],
- Password = data[1]
- };
- await Events.ValidateCredentials(validateCredentialsContext);
- // 认证通过
- if(validateCredentialsContext.Result?.Succeeded == true)
- {
- var ticket = new AuthenticationTicket(validateCredentialsContext.Principal, Scheme.Name);
- return AuthenticateResult.Success(ticket);
- }
- return AuthenticateResult.NoResult();
- }
- catch(FormatException)
- {
- return AuthenticateResult.Fail("Invalid credentials, error format.");
- }
- catch(Exception ex)
- {
- return AuthenticateResult.Fail(ex.Message);
- }
- }
- protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
- {
- var authResult = await HandleAuthenticateOnceSafeAsync();
- var challengeContext = new BasicChallengeContext(Context, Scheme, Options, properties)
- {
- AuthenticateFailure = authResult?.Failure
- };
- await Events.Challenge(challengeContext);
- // 质询已处理
- if (challengeContext.Handled) return;
- var challengeValue = $"{ BasicDefaults.AuthenticationScheme } realm={ Options.Realm }";
- var error = challengeContext.AuthenticateFailure?.Message;
- if(string.IsNullOrWhiteSpace(error))
- {
- // 将错误信息封装到内部
- challengeValue += $"error={ error }";
- }
- Response.StatusCode = (int)HttpStatusCode.Unauthorized;
- Response.Headers.Append(HeaderNames.WWWAuthenticate, challengeValue);
- }
- private string GetCredentials(HttpRequest request)
- {
- string credentials = null;
- string authorization = request.Headers[HeaderNames.Authorization];
- // 存在 Authorization 标头
- if (authorization != null)
- {
- var scheme = BasicDefaults.AuthenticationScheme;
- if (authorization.StartsWith(scheme, StringComparison.OrdinalIgnoreCase))
- {
- credentials = authorization.Substring(scheme.Length).Trim();
- }
- }
- return credentials;
- }
- }
4. 最后, 就是要把封装的接口暴露给用户了, 这里使用扩展方法的形式, 虽然有 4 个方法, 但实际上都是重载, 是同一种行为.
- public static class BasicExtensions
- {
- public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder)
- => builder.AddBasic(BasicDefaults.AuthenticationScheme, _ => { });
- public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action<BasicOptions> configureOptions)
- => builder.AddBasic(BasicDefaults.AuthenticationScheme, configureOptions);
- public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action<BasicOptions> configureOptions)
- => builder.AddBasic(authenticationScheme, displayName: null, configureOptions: configureOptions);
- public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<BasicOptions> configureOptions)
- => builder.AddScheme<BasicOptions, BasicHandler>(authenticationScheme, displayName, configureOptions);
- }
5.Basic 认证库已经封装好了, 我们创建一个 ASP.NET Core WebApi 程序来测试一下吧.
- // 在 ConfigureServices 中配置认证中间件
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddAuthentication(BasicDefaults.AuthenticationScheme)
- .AddBasic(options =>
- {
- options.Realm = "Test Realm";
- options.Events = new BasicEvents
- {
- OnValidateCredentials = context =>
- {
- var user = UserService.Authenticate(context.UserName, context.Password);
- if (user != null)
- {
- // 将用户信息封装到 HttpContext
- var claim = new Claim(ClaimTypes.Name, context.UserName);
- var identity = new ClaimsIdentity(BasicDefaults.AuthenticationScheme);
- identity.AddClaim(claim);
- context.Principal = new ClaimsPrincipal(identity);
- context.Success();
- }
- return Task.CompletedTask;
- }
- };
- });
- }
- // 在 Configure 中启用认证中间件
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- App.UseAuthentication();
- }
对了, 一定要记得为需要认证的 Action 添加 [Authorize] 特性, 否则前面做的一切都是徒劳 +_+
查看源码
来源: https://www.cnblogs.com/xiaoxiaotank/p/11016023.html