一、概述
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抓包情况如下:

上图步骤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