HTTP認證之基本認證——Basic(二)

HTTP認證之基本認證——Basic(一)中介紹了Basic認證的工做原理和流程,接下來就趕忙經過代碼來實踐一下,如下教程基於ASP.NET Core WebApi框架。若有興趣,可查看源碼html

1、準備工做

在開始以前,先把最基本的用戶名密碼校驗邏輯準備好,只有一個認證方法: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; }
}

2、編碼

1.首先,先肯定使用的認證方案爲Basic,並提供默認的的Realmgithub

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

3、封裝爲中間件

ASP.NET Core相比ASP.NET最大的突破大概就是插件配置化了——經過將各個功能封裝成中間件,應用AOP的設計思想配置到應用程序中。如下封裝採用Jwt Bearer封裝規範。ide

  1. 首先封裝常量
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.AuthenticationHandlerui

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

查看源碼

相關文章
相關標籤/搜索
每日一句
    每一个你不满意的现在,都有一个你没有努力的曾经。