在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]
特性,不然前面作的一切都是徒勞+_+