HTTP Basic认证

一、概述

1、理解Http的无状态特性

HTTP是一个无状态的协议,WEB服务器在处理所有传入HTTP请求时,根本就不知道某个请求是否是一个用户的第一次请求与后续请求,或者是另一个用户的请求。 WEB服务器每次在处理请求时,都会按照用户所访问的资源所对应的处理代码,从头到尾执行一遍,然后输出响应内容,WEB服务器根本不会记住已处理了哪些用户的请求,因此,我们通常说HTTP协议是无状态的。

2、为什么需要认证

虽然HTTP协议与WEB服务器是无状态,但我们的业务需求却要求有状态,典型的就是用户登录, 在这种业务需求中,要求WEB服务器端能区分某个请求是不是一个已登录用户发起的,或者当前请求是哪个用户发出的。 在开发WEB应用程序时,我们通常会使用Cookie来保存一些简单的数据供服务端维持必要的状态。总的来说,加入认证的根本原因就是确保请求的合法性以及资源的安全性,如下图:

二、HTTP Basic认证

http认证根据凭证协议的不同,划分为不同的方式。常用的方式有:

  • HTTP基本认证
  • HTTP摘要认证
  • HTTP Bearer认证

本篇文章介绍HTTP基本认证。

1、原理解析

下面通过图详细的了解下HTTP Basic认证过程:

WWW-Authenticate格式如下:WWW-Authenticate: <type> realm=<realm> 其中:

  • WWW-Authenticate 定义了使用何种验证方式去获取对资源的连接,即告诉客户端需要提供凭证才能获取资源。
  • <type>是认证方案,常见的有Basic 、Bearer、 Digest等。
  • Realm指资源的描述。

上图过程2认证失败,返回401的fiddler抓包情况如下:

浏览器根据WWW-Authenticate响应头会弹出一个登录验证的对话框,要求客户端提供用户名和密码进行认证。如下图:

上图步骤3,浏览器将输入的用户名密码用Base64进行编码后,采用非加密的明文方式传送给服务器。格式如下:Authorization: <type> <credentials>。抓包结果如下: 

服务端认证凭证成功后,返回200。

2、优缺点

HTTP Basic认证的缺点:
1.用户名和密码明文(Base64)传输,容易泄露用户信息,尽量配合HTTPS来保证信息传输的安全。
2.容易遭到重放攻击

三、HTTP Baice认证示例

1、新建netcore mvc项目

2、新建User类

记录用户信息

namespace HttpBasicAuthentication.Models
{
    public class User
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
}

3、新建UserAuthenticate.cs

添加具体的认证逻辑

using HttpBasicAuthentication.Models;

namespace HttpBasicAuthentication.Services
{
    /// <summary>
    /// 认证逻辑
    /// </summary>
    public class UserAuthenticate
    {
        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;
        }
    }
}

4、新建BasicDefaults.cs

指定默认认证方案

namespace HttpBasicAuthentication.Models
{
    /// <summary>
    /// 指定认证方案和默认的realme
    /// </summary>
    public class BasicDefaults
    {
        public const string AuthenticationScheme = "Basic";
        public const string AuthenticationRealm = "天气资源";
    }
}

5、新建BasicOptions.cs

封装Basic认证的Options,包括Realm和事件

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;

namespace HttpBasicAuthentication.Middleware
{
    public class BasicOptions : AuthenticationSchemeOptions
    {
        /// <summary>
        /// realme
        /// </summary>
        public string WeatherForecast { get; set; }
        /// <summary>
        /// 事件
        /// </summary>
        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<AuthenticationSchemeOptions>
    {
        public ValidateCredentialsContext(HttpContext context, AuthenticationScheme scheme, AuthenticationSchemeOptions 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;
    }
}

6、新建BasicHandler.cs

封装服务端质询、验证逻辑

using HttpBasicAuthentication.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using System;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace HttpBasicAuthentication.Middleware
{
    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.WeatherForecast }"";
            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;
        }
    }
}

7、新建BasicExtensions.cs

将接口暴露

using HttpBasicAuthentication.Models;
using Microsoft.AspNetCore.Authentication;
using System;

namespace HttpBasicAuthentication.Middleware
{
    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);
    }
}

8、Startup.cs中配置中间件

在 ConfigureServices 中配置认证中间件

public void ConfigureServices(IServiceCollection services)
{
  services.AddControllersWithViews();
  services.AddAuthentication(BasicDefaults.AuthenticationScheme)
  .AddBasic(options =>
  {
    options.WeatherForecast = BasicDefaults.AuthenticationRealm;
    options.Events = new BasicEvents
    {
      OnValidateCredentials = context =>
      {
        var user = UserAuthenticate.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, IWebHostEnvironment env)
{
  app.UseAuthentication();
  app.UseAuthorization();
}

9、Action加入认证

在HomeController的action上加入[Authorize]。

using HttpBasicAuthentication.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Diagnostics;

namespace HttpBasicAuthentication.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }
        [Authorize]
        public IActionResult Index()
        {
            return View();
        }
        [Authorize]
        public IActionResult Privacy()
        {
            return View();
        }
        [Authorize]
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

10、最终项目目录及运行效果

运行项目,效果如下:

输入用户名和密码(用户名和密码一致即可),点击登录,认证成功,记录用户信息并进行重定向:

四、源码下载

源码:https://github.com/qiuxianhu/AuthenticationAndAuthorization

原文地址:https://www.cnblogs.com/qtiger/p/14867023.html