• HTTP认证之基本认证——Basic(二)


    导航

    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最大的突破大概就是插件配置化了——通过将各个功能封装成中间件,应用AOP的设计思想配置到应用程序中。以下封装采用Jwt Bearer封装规范(.Net Core 2.2 类库)。

    Nuget: Microsoft.AspNetCore.Authentication

    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.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]特性,否则前面做的一切都是徒劳+_+

    查看源码

  • 相关阅读:
    Release COM Objects in AE
    图像相关系数
    Geoprocessor edit the featureclasses in memmory
    NetLogo AStar path finding
    IDL+C#三种调用方式
    Dictionary is not like a array
    C# DataGridView 禁止列排序
    工作总结
    (转)常见数据库设计(1)——字典数据
    碎碎念(3)
  • 原文地址:https://www.cnblogs.com/xiaoxiaotank/p/11016023.html
Copyright © 2020-2023  润新知