在HTTP认证之基本认证——Basic(一)中介绍了Basic认证的工做原理和流程,接下来就赶忙经过代码来实践一下,如下教程基于
ASP.NET Core WebApi
框架。若有兴趣,可查看源码html
在开始以前,先把最基本的用户名密码校验逻辑准备好,只有一个认证方法:git
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
,github
public const string AuthenticationScheme = "Basic"; public const string AuthenticationRealm = "Test Realm";
2.而后,解析HTTP Request获取到Authorization
标头app
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]
,大功告成!本身测试一下吧async
ASP.NET Core
相比ASP.NET
最大的突破大概就是插件配置化了——经过将各个功能封装成中间件
,应用AOP
的设计思想配置到应用程序中。如下封装采用Jwt Bearer
封装规范。ide
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
ui
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个方法,但实际上都是重载,是同一种行为。this
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]
特性,不然前面作的一切都是徒劳+_+