.netcore入门10:分析aspnetcore自带的cookie认证原理

环境

  • netcore 3.1.1.0

一. 认证相关名词解释

1.1 Scheme、Claim、ClaimsIdentity、ClaimsPrincipal介绍

  • Scheme(独立于另外三个):身份认证方案(应用中可以有多个方案,比如:AddCookie(options=>{})表示cookie方案,AddJwtBear(options=>{})表示token方案)
  • Claim:一条关键信息,比如:身份证上的身份证号、身份证号上的姓名、驾照上的身份证号
  • ClaimsIdentity:比如你的身份证或驾照
  • ClaimsPrincipal:你自己(HttpContext.User)

总结:一个ClaimsPrincipal可以拥有多个ClaimsIdentity,每个ClaimsIdentity又可以有多个Claim
参照:https://www.cnblogs.com/dudu/p/6367303.html
https://andrewlock.net/introduction-to-authentication-with-asp-net-core/

1.2 关于Authentication和Authorization

  • Authentication:身份认证,能证明"你是谁"
  • Authorization:授权,能决定你“你有没有权限”

授权依赖于身份认证,尽管它们之间有联系,但是在aspnetcore中这是两块内容。

二. aspnetcore自带的cookie认证的使用方法(asp.net core api 3.1)

  • 第一步: 注册Authentication服务、Cookie认证方案startup.cs
public void ConfigureServices(IServiceCollection services)
{
     services.AddControllers();
     services.AddAuthentication(options =>
     {
         options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
     }).AddCookie(options =>
     {
         //将未登陆的请求重定向到静态页面
         options.LoginPath = "/login.html";
     });
}
  • 第二步:注册中间件startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
	//这里开启静态资源服务器用来承载login.html页面
    app.UseStaticFiles();
	//开启认证中间件
    app.UseAuthentication();
    app.UseRouting();
    //开启授权中间件
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
  • 第三步:新建控制器,并打上Authorize标记
namespace WebApplication1.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class DemoController : ControllerBase
    {
        [HttpGet]
        [Authorize]
        public string Index()
        {
            return $"DemoController.Index() {DateTime.Now}";
        }
    }
}
  • 第四步:编写 /Account/Login
namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class AccountController : ControllerBase
    {
        [HttpPost]
        public string Login([FromForm] string username, [FromForm] string pwd)
        {
            if (username == "admin" && pwd == "1")
            {
                var identity = new ClaimsIdentity(new Claim[] { new Claim("name", "admin") }, CookieAuthenticationDefaults.AuthenticationScheme);
                var user = new ClaimsPrincipal(identity);
                HttpContext.SignInAsync(user);
                return "登录成功!";
            }
            return "登录失败";
        }
    }
}
  • 第五步:编写登录页面 wwwroot/login.html
    新建文件夹wwwroot,然后再新建文件wwwroot/login.html,如下:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>登录</title>
</head>
<body>
    <form action="/api/account/login" method="post" enctype="application/x-www-form-urlencoded">
        登录名:<input type="text" name="username" /><br />
        密码:<input type="password" name="pwd" />
        <input type="submit" value="登录" />
    </form>
</body>
</html>
  • 第六步:实现效果
    先访问http://localhost:5000/api/demo/index,被拦截跳转到了login.html页面
    然后,输入admin1后提交登录,显示登录成功
    在这里插入图片描述

三. 关于Cookie的几个路径配置说明

  • LoginPath:这个应该指向的是一个页面和一个后台处理方法(默认:/Account/Login)
    a. 当验证失败的时候,会重定向到这个页面,比如:https://localhost:5001/Account/Login?ReturnUrl=%2F
    b. 发向这个地址的登录请求经验证成功后会自动跳转到其他的页面地址(ReturnUrl参数中指定的)
    c. LoginPath的使用场景
    • 使用场景1(aspx): /Account/Login.aspx (既是页面也是处理请求)
    • 使用场景2(mvc):/Account/Login(get),当有用户名和密码时处理登录完跳转ReturnUrl,否则显示登录页(Return View())
    • 使用场景3(mvc):/Account/Login(get)登录页面,/Account/Login(post)处理登录请求后跳转ReturnUrl
  • LogoutPath:指向一个地址(退出登录处理地址,默认:/Account/Logout),在这里处理退出登录的请求后自动跳转到ReturnUrl页面上

四、Cookie认证源码解析

参照:

4.1 流程概述:

首先在http的请求管道里注册身份认证和授权中间件(app.UseAuthentication();app.UseAuthorization();),中间件拦截之后就根据你注入的认证服务(services.AddAuthentication())和认证方案(AddCookie())进行身份认证,如果获取到了身份的话就包装成ClaimsPrincipal放在HttpContext.User上,没获取到的话也要继续向下走。接着授权中间件找到了Controller.Action观察是否有[Authorize]标记,如果有的话就检查是否认证了身份,没有认证身份的就按照LoginPath加上ReturnUrl重定向(比如:https://localhost:5001/Account/Login?ReturnUrl=%2F)

4.2 源码骨架分析:

这里将Authentication和Authorization作为框架对待,而Cookie认证方式作为自定义的扩展对待。代码在它们之间的执行流程和关键点如下所示:

在这里插入图片描述

4.3 源码分析:

  • app.UseAuthentication()说起
    这里向Http请求管道注册了中间件AuthenticationMiddleware,查看它的源代码:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authentication
{
   public class AuthenticationMiddleware
   {
       private readonly RequestDelegate _next;

       public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
       {
           if (next == null)
           {
               throw new ArgumentNullException(nameof(next));
           }
           if (schemes == null)
           {
               throw new ArgumentNullException(nameof(schemes));
           }

           _next = next;
           Schemes = schemes;
       }

       public IAuthenticationSchemeProvider Schemes { get; set; }

       public async Task Invoke(HttpContext context)
       {
           context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
           {
               OriginalPath = context.Request.Path,
               OriginalPathBase = context.Request.PathBase
           });

           // Give any IAuthenticationRequestHandler schemes a chance to handle the request
           var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
           foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
           {
               var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
               if (handler != null && await handler.HandleRequestAsync())
               {
                   return;
               }
           }

           var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
           if (defaultAuthenticate != null)
           {
               var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
               if (result?.Principal != null)
               {
                   context.User = result.Principal;
               }
           }

           await _next(context);
       }
   }
}

重点查看Invoke里面的内容,那个IAuthenticationRequestHandler的处理逻辑在cookie中没有用到,这里跳过,直接看var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();,这里是获取默认的认证方案的,Schemes是我们在AddAuthentication里注册进去的,为什么这么说呢?看下面的代码:
在这里插入图片描述在这里插入图片描述
接下来就查看GetDefaultAuthenticateSchemeAsync方法是从哪获取到默认方案的,查看AuthenticationSchemeProvider的部分代码:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
    {
        public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
            : this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
        {
        }
        protected AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
        {
            _options = options.Value;

            _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
            _requestHandlers = new List<AuthenticationScheme>();

            foreach (var builder in _options.Schemes)
            {
                var scheme = builder.Build();
                AddScheme(scheme);
            }
        }

        private readonly AuthenticationOptions _options;
        private readonly object _lock = new object();

        private readonly IDictionary<string, AuthenticationScheme> _schemes;
        private readonly List<AuthenticationScheme> _requestHandlers;
        private IEnumerable<AuthenticationScheme> _schemesCopy = Array.Empty<AuthenticationScheme>();
        private IEnumerable<AuthenticationScheme> _requestHandlersCopy = Array.Empty<AuthenticationScheme>();

        private Task<AuthenticationScheme> GetDefaultSchemeAsync()
            => _options.DefaultScheme != null
            ? GetSchemeAsync(_options.DefaultScheme)
            : Task.FromResult<AuthenticationScheme>(null);

        public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
            => _options.DefaultAuthenticateScheme != null
            ? GetSchemeAsync(_options.DefaultAuthenticateScheme)
            : GetDefaultSchemeAsync();

       ......
        public virtual Task<AuthenticationScheme> GetSchemeAsync(string name)
            => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
        public virtual void AddScheme(AuthenticationScheme scheme)
        {
            if (_schemes.ContainsKey(scheme.Name))
            {
                throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
            }
            lock (_lock)
            {
                if (_schemes.ContainsKey(scheme.Name))
                {
                    throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
                }
                if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
                {
                    _requestHandlers.Add(scheme);
                    _requestHandlersCopy = _requestHandlers.ToArray();
                }
                _schemes[scheme.Name] = scheme;
                _schemesCopy = _schemes.Values.ToArray();
            }
        }
    }
}

可以看到,最终是从_schemes集合里取到的scheme(关键字就是我们配置的默认方案名称)。那么这些Scheme是怎么添加进来的呢?这要看AddCookie()的代码了:
在这里插入图片描述
那么这个AuthenticationBuilder从哪来的呢,它就是services.AddAuthentication()返回的,我们直接看AuthenticationBuilder的代码:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationBuilder
    {
        public AuthenticationBuilder(IServiceCollection services)
            => Services = services;
            
        public virtual IServiceCollection Services { get; }

        private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
            where TOptions : AuthenticationSchemeOptions, new()
            where THandler : class, IAuthenticationHandler
        {
            Services.Configure<AuthenticationOptions>(o =>
            {
                o.AddScheme(authenticationScheme, scheme => {
                    scheme.HandlerType = typeof(THandler);
                    scheme.DisplayName = displayName;
                });
            });
            if (configureOptions != null)
            {
                Services.Configure(authenticationScheme, configureOptions);
            }
            Services.AddOptions<TOptions>(authenticationScheme).Validate(o => {
                o.Validate(authenticationScheme);
                return true;
            });
            Services.AddTransient<THandler>();
            return this;
        }
        public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
            where TOptions : AuthenticationSchemeOptions, new()
            where THandler : AuthenticationHandler<TOptions>
            => AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);

        ......
    }
}

从上面的源码中我们可以看到关键代码在AddSchemeHelper()方法中:

private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
    where TOptions : AuthenticationSchemeOptions, new()
    where THandler : class, IAuthenticationHandler
{
    Services.Configure<AuthenticationOptions>(o =>
    {
        o.AddScheme(authenticationScheme, scheme => {
            scheme.HandlerType = typeof(THandler);
            scheme.DisplayName = displayName;
        });
    });
    ...
}

这里的o就是AuthenticationOptions,我们再看它的代码:

using System;
using System.Collections.Generic;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationOptions
    {
        private readonly IList<AuthenticationSchemeBuilder> _schemes = new List<AuthenticationSchemeBuilder>();

        /// <summary>
        /// Returns the schemes in the order they were added (important for request handling priority)
        /// </summary>
        public IEnumerable<AuthenticationSchemeBuilder> Schemes => _schemes;

        /// <summary>
        /// Maps schemes by name.
        /// </summary>
        public IDictionary<string, AuthenticationSchemeBuilder> SchemeMap { get; } = new Dictionary<string, AuthenticationSchemeBuilder>(StringComparer.Ordinal);

        public void AddScheme(string name, Action<AuthenticationSchemeBuilder> configureBuilder)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }
            if (configureBuilder == null)
            {
                throw new ArgumentNullException(nameof(configureBuilder));
            }
            if (SchemeMap.ContainsKey(name))
            {
                throw new InvalidOperationException("Scheme already exists: " + name);
            }

            var builder = new AuthenticationSchemeBuilder(name);
            configureBuilder(builder);
            _schemes.Add(builder);
            SchemeMap[name] = builder;
        }
        ......
    }
}

可以看到最终把新创建的AuthenticationSchemeBuilder放进了SchemeMap(IDictionary<string, AuthenticationSchemeBuilder>)中。
这里有个就要问了“为什么不是创建AuthenticationScheme?AuthenticationSchemeProvider里又是怎么获取到的呢?”
带着问题让我们看下AuthenticationSchemeProvider里的构造函数都注入了什么,注入后又是怎么处理的:
在这里插入图片描述
现在就知道AddCookie()添加的Scheme怎么被AuthenticationSchemeProvider找到的了吧,那么AuthenticationMiddleware中间件也就找到了Cookie的AuthenticationScheme了。
接下来再看AuthenticationMiddleware的代码:
在这里插入图片描述
接下来就是将获取到的Cookie的Scheme的Name传给HttpContext.AuthenticateAsync()方法了:

namespace Microsoft.AspNetCore.Authentication
{
    public static class AuthenticationHttpContextExtensions
    {
        public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context) =>
            context.AuthenticateAsync(scheme: null);

        public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);
        ......
    }
}

这里其实就是从容器里找到IAuthenticationService然后调用它的方法,这个服务在AddAuthentication()的时候就已经注册到容器中了(可以看上面的截图),这里直接看AuthenticationService.AuthenticateAsync()

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationService : IAuthenticationService
    {
        public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform, IOptions<AuthenticationOptions> options)
        {
            Schemes = schemes;
            Handlers = handlers;
            Transform = transform;
            Options = options.Value;
        }
        public IAuthenticationSchemeProvider Schemes { get; }

        public IAuthenticationHandlerProvider Handlers { get; }

        public IClaimsTransformation Transform { get; }

        public AuthenticationOptions Options { get; }
        public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
        {
            if (scheme == null)
            {
                var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
                scheme = defaultScheme?.Name;
                if (scheme == null)
                {
                    throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
                }
            }

            var handler = await Handlers.GetHandlerAsync(context, scheme);
            if (handler == null)
            {
                throw await CreateMissingHandlerException(scheme);
            }

            var result = await handler.AuthenticateAsync();
            if (result != null && result.Succeeded)
            {
                var transformed = await Transform.TransformAsync(result.Principal);
                return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
            }
            return result;
        }
        ......
    }
}

那么此时就去寻找Handlers,也就是容器中的IAuthenticationHandlerProvider,这个服务我们在AddAuthentication()里也已经注册过了,直接看它的代码:

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
    {
        public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
        {
            Schemes = schemes;
        }
        public IAuthenticationSchemeProvider Schemes { get; }

        private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>(StringComparer.Ordinal);
        
        public async Task<IAuthenticationHandler> GetHandlerAsync(HttpContext context, string authenticationScheme)
        {
            if (_handlerMap.ContainsKey(authenticationScheme))
            {
                return _handlerMap[authenticationScheme];
            }

            var scheme = await Schemes.GetSchemeAsync(authenticationScheme);
            if (scheme == null)
            {
                return null;
            }
            var handler = (context.RequestServices.GetService(scheme.HandlerType) ??
                ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType))
                as IAuthenticationHandler;
            if (handler != null)
            {
                await handler.InitializeAsync(scheme, context);
                _handlerMap[authenticationScheme] = handler;
            }
            return handler;
        }
    }
}

其实这里Schemes.GetSchemeAsync(authenticationScheme)这个过程上面已经分析过了,在AddCookie的时候注册了Scheme和CookieAuthenticationHandler,所以紧接着下面获取到的handler就是CookieAuthenticationHandler,让我们往下进行,也就是分析var result = await handler.AuthenticateAsync();(AuthenticationService.cs里的)。
现在我们应该查看CookieAuthenticationHandler类的AuthenticateAsync方法,这个方法是在父类AuthenticationHandler定义的,那么一路看过去:

namespace Microsoft.AspNetCore.Authentication
{
    public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
    {
	......
	 public async Task<AuthenticateResult> AuthenticateAsync()
        {
            var target = ResolveTarget(Options.ForwardAuthenticate);
            if (target != null)
            {
                return await Context.AuthenticateAsync(target);
            }

            // Calling Authenticate more than once should always return the original value.
            var result = await HandleAuthenticateOnceAsync();
            if (result?.Failure == null)
            {
                var ticket = result?.Ticket;
                if (ticket?.Principal != null)
                {
                    Logger.AuthenticationSchemeAuthenticated(Scheme.Name);
                }
                else
                {
                    Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name);
                }
            }
            else
            {
                Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message);
            }
            return result;
        }

	protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
        {
            if (_authenticateTask == null)
            {
                _authenticateTask = HandleAuthenticateAsync();
            }

            return _authenticateTask;
        }

	protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();
	......
    }
}

这个时候我们要再回到CookieAuthenticationHandler查看HandleAuthenticateAsync方法:

namespace Microsoft.AspNetCore.Authentication.Cookies
{
    public class CookieAuthenticationHandler : SignInAuthenticationHandler<CookieAuthenticationOptions>
    {
	......
	protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var result = await EnsureCookieTicket();
            if (!result.Succeeded)
            {
                return result;
            }

            var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket);
            await Events.ValidatePrincipal(context);

            if (context.Principal == null)
            {
                return AuthenticateResult.Fail("No principal.");
            }

            if (context.ShouldRenew)
            {
                RequestRefresh(result.Ticket, context.Principal);
            }

            return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name));
        }
	private Task<AuthenticateResult> EnsureCookieTicket()
        {
            // We only need to read the ticket once
            if (_readCookieTask == null)
            {
                _readCookieTask = ReadCookieTicket();
            }
            return _readCookieTask;
        }
	 private async Task<AuthenticateResult> ReadCookieTicket()
        {
            var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
            if (string.IsNullOrEmpty(cookie))
            {
                return AuthenticateResult.NoResult();
            }

            var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
            if (ticket == null)
            {
                return AuthenticateResult.Fail("Unprotect ticket failed");
            }

            if (Options.SessionStore != null)
            {
                var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
                if (claim == null)
                {
                    return AuthenticateResult.Fail("SessionId missing");
                }
                _sessionKey = claim.Value;
                ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
                if (ticket == null)
                {
                    return AuthenticateResult.Fail("Identity missing in session store");
                }
            }

            var currentUtc = Clock.UtcNow;
            var expiresUtc = ticket.Properties.ExpiresUtc;

            if (expiresUtc != null && expiresUtc.Value < currentUtc)
            {
                if (Options.SessionStore != null)
                {
                    await Options.SessionStore.RemoveAsync(_sessionKey);
                }
                return AuthenticateResult.Fail("Ticket expired");
            }

            CheckForRefresh(ticket);

            // Finally we have a valid ticket
            return AuthenticateResult.Success(ticket);
        }
	......
    }
}

可以看到最终是从Request中读取到了Cookie并且去除了数据保护,得到ticket。
现在我们可以回到AuthenticationMiddleware中间件里了:
在这里插入图片描述
可以看到,验证完了之后把得到的ticket的Principal赋值给HttpContext.User,并且继续往下执行(无论验证通过与否)。

五. 授权服务

现在身份认证通过了,该授权了,当授权中间件app.UseAuthorization();发现未认证身份的请求访问[Authorize]的时候就会调用HttpContext.ChallengeAsync(),就像HttpContext.AuthenticateAsync()一样最终会执行CookieAuthenticationHandler.HandleChallengeAsync()
在这里插入图片描述
可以看到最终使用了配置的LoginPath进行了重定向。
这里的Events就是CookieAuthenticationEvents,看它的处理代码:

namespace Microsoft.AspNetCore.Authentication.Cookies
{
    public class CookieAuthenticationEvents
    {
	......
        public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
        {
            if (IsAjaxRequest(context.Request))
            {
                context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
                context.Response.StatusCode = 401;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }
            return Task.CompletedTask;
        };
        private static bool IsAjaxRequest(HttpRequest request)
        {
            return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
                string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
        }
        public virtual Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogin(context);
	......
    }
}

可以看到这里判断是否是ajax,然后决定返回401还是重定向,其实我们在注册cookie方案的时候可以进行配置:

services.AddAuthentication().AddCookie(opt =>
{
     opt.LoginPath = "account/mylogin";
     opt.Events.OnRedirectToLogin = context =>
     {
           context.Response.Redirect(context.RedirectUri);
           return Task.CompletedTask;
     };
});

六、用户与Authentication的交互调用情况

从上面的分析中我们得知,身份认证主要有如下几个接口组成

  • AuthenticateAsync():当需要对request进行身份识别时调用(由UseAuthentication()中间件发起)
  • ChallengeAsync():当授权失败时调用(由UseAuthorization()中间件配合[Authorize]发起)
  • ForbidAsync():当禁止访问时调用(用户代码或其他发起)
  • SignInAsync():当需要将登陆凭证注册到应用中的时候调用(用户代码发起)
  • SignOutAsync():当需要将登陆凭证从应用中移除的时候调用(用户代码发起)

上面这些接口的调用入口都集中放在了HttpContext上,作为统一的调用方式对外提供,比如:

HttpContext.AuthenticateAsync();
HttpContext.ChallengeAsync();
HttpContext.ForbidAsync();
HttpContext.AuthenticateAsync();
HttpContext.SignInAsync();

下面就看一下HttpContext是如何处理这些调用请求的AuthenticationHttpContextExtensions.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authentication
{
    public static class AuthenticationHttpContextExtensions
    {
        public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context) =>
            context.AuthenticateAsync(scheme: null);

        public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);

        public static Task ChallengeAsync(this HttpContext context, string scheme) =>
            context.ChallengeAsync(scheme, properties: null);

        public static Task ChallengeAsync(this HttpContext context) =>
            context.ChallengeAsync(scheme: null, properties: null);

        public static Task ChallengeAsync(this HttpContext context, AuthenticationProperties properties) =>
            context.ChallengeAsync(scheme: null, properties: properties);

        public static Task ChallengeAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().ChallengeAsync(context, scheme, properties);

        public static Task ForbidAsync(this HttpContext context, string scheme) =>
            context.ForbidAsync(scheme, properties: null);

        public static Task ForbidAsync(this HttpContext context) =>
            context.ForbidAsync(scheme: null, properties: null);

        public static Task ForbidAsync(this HttpContext context, AuthenticationProperties properties) =>
            context.ForbidAsync(scheme: null, properties: properties);

        public static Task ForbidAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().ForbidAsync(context, scheme, properties);

        public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal) =>
            context.SignInAsync(scheme, principal, properties: null);

        public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) =>
            context.SignInAsync(scheme: null, principal: principal, properties: null);

        public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties properties) =>
            context.SignInAsync(scheme: null, principal: principal, properties: properties);

        public static Task SignInAsync(this HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);

        public static Task SignOutAsync(this HttpContext context) => context.SignOutAsync(scheme: null, properties: null);

        public static Task SignOutAsync(this HttpContext context, AuthenticationProperties properties) => context.SignOutAsync(scheme: null, properties: properties);

        public static Task SignOutAsync(this HttpContext context, string scheme) => context.SignOutAsync(scheme, properties: null);

        public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().SignOutAsync(context, scheme, properties);

        public static Task<string> GetTokenAsync(this HttpContext context, string scheme, string tokenName) =>
            context.RequestServices.GetRequiredService<IAuthenticationService>().GetTokenAsync(context, scheme, tokenName);

        public static Task<string> GetTokenAsync(this HttpContext context, string tokenName) =>            context.RequestServices.GetRequiredService<IAuthenticationService>().GetTokenAsync(context, tokenName);
    }
}

从HttpContext的处理情况来看,它们都是在调用IAuthenticationService的方法,那么再看一下AuthenticationService的处理情况:

using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationService : IAuthenticationService
    {
        public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform, IOptions<AuthenticationOptions> options)
        {
            Schemes = schemes;
            Handlers = handlers;
            Transform = transform;
            Options = options.Value;
        }
        public IAuthenticationSchemeProvider Schemes { get; }
        public IAuthenticationHandlerProvider Handlers { get; }
        public IClaimsTransformation Transform { get; }
        public AuthenticationOptions Options { get; }
        public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
        {
            if (scheme == null)
            {
                var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
                scheme = defaultScheme?.Name;
                if (scheme == null)
                {
                    throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
                }
            }

            var handler = await Handlers.GetHandlerAsync(context, scheme);
            if (handler == null)
            {
                throw await CreateMissingHandlerException(scheme);
            }

            var result = await handler.AuthenticateAsync();
            if (result != null && result.Succeeded)
            {
                var transformed = await Transform.TransformAsync(result.Principal);
                return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
            }
            return result;
        }
        public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
        {
            if (scheme == null)
            {
                var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
                scheme = defaultChallengeScheme?.Name;
                if (scheme == null)
                {
                    throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
                }
            }

            var handler = await Handlers.GetHandlerAsync(context, scheme);
            if (handler == null)
            {
                throw await CreateMissingHandlerException(scheme);
            }

            await handler.ChallengeAsync(properties);
        }
        public virtual async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
        {
            if (scheme == null)
            {
                var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync();
                scheme = defaultForbidScheme?.Name;
                if (scheme == null)
                {
                    throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
                }
            }

            var handler = await Handlers.GetHandlerAsync(context, scheme);
            if (handler == null)
            {
                throw await CreateMissingHandlerException(scheme);
            }

            await handler.ForbidAsync(properties);
        }
        public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
        {
            if (principal == null)
            {
                throw new ArgumentNullException(nameof(principal));
            }

            if (Options.RequireAuthenticatedSignIn)
            {
                if (principal.Identity == null)
                {
                    throw new InvalidOperationException("SignInAsync when principal.Identity == null is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true.");
                }
                if (!principal.Identity.IsAuthenticated)
                {
                    throw new InvalidOperationException("SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true.");
                }
            }

            if (scheme == null)
            {
                var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync();
                scheme = defaultScheme?.Name;
                if (scheme == null)
                {
                    throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
                }
            }

            var handler = await Handlers.GetHandlerAsync(context, scheme);
            if (handler == null)
            {
                throw await CreateMissingSignInHandlerException(scheme);
            }

            var signInHandler = handler as IAuthenticationSignInHandler;
            if (signInHandler == null)
            {
                throw await CreateMismatchedSignInHandlerException(scheme, handler);
            }

            await signInHandler.SignInAsync(principal, properties);
        }
        public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
        {
            if (scheme == null)
            {
                var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync();
                scheme = defaultScheme?.Name;
                if (scheme == null)
                {
                    throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
                }
            }

            var handler = await Handlers.GetHandlerAsync(context, scheme);
            if (handler == null)
            {
                throw await CreateMissingSignOutHandlerException(scheme);
            }

            var signOutHandler = handler as IAuthenticationSignOutHandler;
            if (signOutHandler == null)
            {
                throw await CreateMismatchedSignOutHandlerException(scheme, handler);
            }

            await signOutHandler.SignOutAsync(properties);
        }

        private async Task<Exception> CreateMissingHandlerException(string scheme)
        {
            var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name));

            var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?";

            if (string.IsNullOrEmpty(schemes))
            {
                return new InvalidOperationException(
                    $"No authentication handlers are registered." + footer);
            }

            return new InvalidOperationException(
                $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer);
        }

        private async Task<string> GetAllSignInSchemeNames()
        {
            return string.Join(", ", (await Schemes.GetAllSchemesAsync())
                .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType))
                .Select(sch => sch.Name));
        }

        private async Task<Exception> CreateMissingSignInHandlerException(string scheme)
        {
            var schemes = await GetAllSignInSchemeNames();

            // CookieAuth is the only implementation of sign-in.
            var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";

            if (string.IsNullOrEmpty(schemes))
            {
                return new InvalidOperationException(
                    $"No sign-in authentication handlers are registered." + footer);
            }

            return new InvalidOperationException(
                $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer);
        }

        private async Task<Exception> CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler)
        {
            var schemes = await GetAllSignInSchemeNames();

            var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. ";

            if (string.IsNullOrEmpty(schemes))
            {
                // CookieAuth is the only implementation of sign-in.
                return new InvalidOperationException(mismatchError
                    + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?");
            }

            return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}.");
        }

        private async Task<string> GetAllSignOutSchemeNames()
        {
            return string.Join(", ", (await Schemes.GetAllSchemesAsync())
                .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType))
                .Select(sch => sch.Name));
        }

        private async Task<Exception> CreateMissingSignOutHandlerException(string scheme)
        {
            var schemes = await GetAllSignOutSchemeNames();

            var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";

            if (string.IsNullOrEmpty(schemes))
            {
                // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
                return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer);
            }

            return new InvalidOperationException(
                $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer);
        }

        private async Task<Exception> CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler)
        {
            var schemes = await GetAllSignOutSchemeNames();

            var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. ";

            if (string.IsNullOrEmpty(schemes))
            {
                // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
                return new InvalidOperationException(mismatchError
                    + $"Did you forget to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?");
            }

            return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}.");
        }
    }
}

观察到AuthenticationService就是先获取处理的方案(认证、登录、登出、禁止、询问都需要各自的方案),然后根据方案获取IAuthenticationHandler,然后调用它的同名方法,不过我们发现IAuthenticationHandler中只定义了认证、询问、禁止三个接口,而登录和登出并没有定义。经过探索发现登录和登出的方法分别在IAuthenticationSignInHandlerIAuthenticationSignOutHandler方法中定义了,它们的代码如下:

using System.Security.Claims;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Authentication
{
    public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler
    {
        Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties);
    }
    public interface IAuthenticationSignOutHandler : IAuthenticationHandler
    {
        Task SignOutAsync(AuthenticationProperties properties);
    }
    public interface IAuthenticationHandler
    {
        Task InitializeAsync(AuthenticationScheme scheme, HttpContext context);
        Task<AuthenticateResult> AuthenticateAsync();
        Task ChallengeAsync(AuthenticationProperties properties);
        Task ForbidAsync(AuthenticationProperties properties);
    }
}

这些接口的定义都全了,那么它们实现的结构是怎么样的呢?
在这里插入图片描述
可以看到,CookieAuthenticationHandler最终全部实现了,所以在注册Cookie方案(也就是AddCookie())的时候只添加一个CookieAuthenticationHandler就可以了!

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackletter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值