欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-30 09:38:45
...

环境

  • netcore 3.1.1.0
  • vs2019 16.4.5

一. 认证相关名词解释

Scheme、Claim、ClaimsIdentity、ClaimsPrincipal介绍

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

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

二. aspnetcore自带的cookie认证的使用方法(mvc项目)

  • 第一步: 注册Authentication服务、Cookie认证方案startup.cs
public void ConfigureServices(IServiceCollection services)
{
     //mvc应用
     services.AddControllersWithViews();
     services.AddAuthentication(options =>
     {
         options.DefaultScheme= CookieAuthenticationDefaults.AuthenticationScheme;
     }).AddCookie(options =>
     {
         //这里设置的默认的地址(也可以不写)
         options.LoginPath = CookieAuthenticationDefaults.LoginPath;
         options.LogoutPath = CookieAuthenticationDefaults.LogoutPath;
     });
 }
  • 第二步:注册中间件startup.cs
 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 {
     if (env.IsDevelopment())
     {
         app.UseDeveloperExceptionPage();
     }
     else
     {
         app.UseExceptionHandler("/Home/Error");
         // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
         app.UseHsts();
     }
     app.UseHttpsRedirection();
     app.UseStaticFiles();

     app.UseRouting();
     //开启身份认证
     app.UseAuthentication();
     //开启授权验证([Authorize])
     app.UseAuthorization();

     app.UseEndpoints(endpoints =>
     {
         endpoints.MapControllerRoute(
             name: "default",
             pattern: "{controller=Home}/{action=Index}/{id?}");
     });
  • 第三步:在需要授权的Action上打上标记[Authorize]
    .netcore入门10:分析aspnetcore自带的cookie认证原理
  • 第四步:编写 /Account/Login/Account/Logout
    .netcore入门10:分析aspnetcore自带的cookie认证原理
    .netcore入门10:分析aspnetcore自带的cookie认证原理

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

  • LoginPath:这个应该指向的是一个页面和一个后台处理方法(默认:/Account/Login)
    a. 当验证失败的时候,会重定向到这个页面,比如:https://localhost:5001/Account/Login?ReturnUrl=%2F
    b. 发向这个地址的登录请求经验证成功后会自动跳转到其他的页面地址(ReturnUrl参数中指定的)
  • LogoutPath:指向一个地址(退出登录处理地址,默认:/Account/Logout),在这里处理退出登录的请求后自动跳转到ReturnUrl页面上

四、Cookie认证源码解析

参照:

Authentication:身份认证,能证明"你是谁"
Authorization:授权,能决定你“你有没有权限”
授权依赖于身份认证,在aspnetcore中这是两块

流程概述:

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

源码分析:

  • 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里面的内容,直接看var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();,这里是获取默认的认证方案的,Schemes是我们在AddAuthentication里注册进去的,为什么这么说呢?看下面的代码:
.netcore入门10:分析aspnetcore自带的cookie认证原理.netcore入门10:分析aspnetcore自带的cookie认证原理
接下来就查看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()的代码了:
.netcore入门10:分析aspnetcore自带的cookie认证原理
那么这个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里的构造函数都注入了什么,注入后又是怎么处理的:
.netcore入门10:分析aspnetcore自带的cookie认证原理
现在就知道AddCookie()添加的Scheme怎么被AuthenticationSchemeProvider找到的了吧,那么AuthenticationMiddleware中间件也就找到了Cookie的AuthenticationScheme了。
接下来再看AuthenticationMiddleware的代码:
.netcore入门10:分析aspnetcore自带的cookie认证原理
接下来就是将获取到的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中间件里了:
.netcore入门10:分析aspnetcore自带的cookie认证原理
可以看到,验证完了之后把得到的ticket的Principal赋值给HttpContext.User,并且继续往下执行(无论验证通过与否)。

五. 授权服务

现在身份认证通过了,该授权了,当授权中间件app.UseAuthorization();发现未认证身份的请求访问[Authorize]的时候就会调用HttpContext.ChallengeAsync(),就像HttpContext.AuthenticateAsync()一样最终会执行CookieAuthenticationHandler.HandleChallengeAsync()
.netcore入门10:分析aspnetcore自带的cookie认证原理
可以看到最终使用了配置的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;
     };
});
相关标签: .netcore