“Identity.External”的 ASP.NET Core 标识异常

Posted

技术标签:

【中文标题】“Identity.External”的 ASP.NET Core 标识异常【英文标题】:ASP.NET Core Identity Exception for "Identity.External" 【发布时间】:2018-12-16 00:36:54 【问题描述】:

我在使用 ASP.NET Core 2.1 网站时遇到了一个奇怪的问题。当我登录它并在 30 分钟后刷新它时,我总是会抛出这个异常:

InvalidOperationException:没有为方案“Identity.External”注册注销身份验证处理程序。注册的注销方案是:Identity.Application。您是否忘记调用 AddAuthentication().AddCookies("Identity.External",...)?

我没有注册Identity.External 是正确的,但我也不希望它注册。为什么它一直试图退出?以下是我注册 cookie 的方式:

services.AddAuthentication(
    o => 
        o.DefaultScheme = IdentityConstants.ApplicationScheme;
    ).AddCookie(IdentityConstants.ApplicationScheme,
    o => 
        o.Events = new CookieAuthenticationEvents 
            OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
        ;
    );

services.ConfigureApplicationCookie(
    o => 
        o.Cookie.Expiration = TimeSpan.FromHours(2);
        o.Cookie.HttpOnly = true;
        o.Cookie.SameSite = SameSiteMode.Strict;
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;

        o.AccessDeniedPath = "/admin";
        o.LoginPath = "/admin";
        o.LogoutPath = "/admin/sign-out";
        o.SlidingExpiration = true;
    );

有人可以为我指出如何解决这个问题的正确方向吗?

更新

这里是 @Edward 在 cmets 中要求的完整代码和使用过程。为简洁起见,我省略了一些部分。

Startup.cs

public sealed class Startup 
    public void ConfigureServices(
        IServiceCollection services) 
        //  ...
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddApplicationIdentity();
        services.AddScoped<ApplicationSignInManager>();

        services.Configure<IdentityOptions>(
            o => 
                o.Password.RequiredLength = 8;

                o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                o.Lockout.MaxFailedAccessAttempts = 5;
            );
        services.ConfigureApplicationCookie(
            o => 
                o.Cookie.Name = IdentityConstants.ApplicationScheme;
                o.Cookie.Expiration = TimeSpan.FromHours(2);
                o.Cookie.HttpOnly = true;
                o.Cookie.SameSite = SameSiteMode.Strict;
                o.Cookie.SecurePolicy = CookieSecurePolicy.Always;

                o.AccessDeniedPath = "/admin";
                o.LoginPath = "/admin";
                o.LogoutPath = "/admin/sign-out";
                o.SlidingExpiration = true;
            );
        //  ...
    

    public void Configure(
        IApplicationBuilder app) 
        //  ...
        app.UseAuthentication();
        //  ...
    

ServiceCollectionExtensions.cs

public static class ServiceCollectionExtensions 
    public static IdentityBuilder AddApplicationIdentity(
        this IServiceCollection services) 
        services.AddAuthentication(
            o => 
                o.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
                o.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
                o.DefaultForbidScheme = IdentityConstants.ApplicationScheme;
                o.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
                o.DefaultSignOutScheme = IdentityConstants.ApplicationScheme;
            ).AddCookie(IdentityConstants.ApplicationScheme,
            o => 
                o.Events = new CookieAuthenticationEvents 
                    OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
                ;
            );

        services.TryAddScoped<SignInManager<User>, ApplicationSignInManager>();
        services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
        services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
        services.TryAddScoped<IdentityErrorDescriber>();
        services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<User>>();
        services.TryAddScoped<IUserClaimsPrincipalFactory<User>, UserClaimsPrincipalFactory<User>>();
        services.TryAddScoped<UserManager<User>>();
        services.TryAddScoped<IUserStore<User>, ApplicationUserStore>();

        return new IdentityBuilder(typeof(User), services);
    

DefaultController.cs

[Area("Admin")]
public sealed class DefaultController :
    AdminControllerBase 
    [HttpPost, AllowAnonymous]
    public async Task<IActionResult> SignIn(
        SignIn.Command command) 
        var result = await Mediator.Send(command);

        if (result.Succeeded) 
            return RedirectToAction("Dashboard", new 
                area = "Admin"
            );
        

        return RedirectToAction("SignIn", new 
            area = "Admin"
        );
    

    [HttpGet, ActionName("sign-out")]
    public async Task<IActionResult> SignOut() 
        await Mediator.Send(new SignOut.Command());

        return RedirectToAction("SignIn", new 
            area = "Admin"
        );
    

SignIn.cs

public sealed class SignIn 
    public sealed class Command :
        IRequest<SignInResult> 
        public string Password  get; set; 
        public string Username  get; set; 
    

    public sealed class CommandHandler :
        HandlerBase<Command, SignInResult> 
        private ApplicationSignInManager SignInManager  get; 

        public CommandHandler(
            DbContext context,
            ApplicationSignInManager signInManager)
            : base(context) 
            SignInManager = signInManager;
        

        protected override SignInResult Handle(
            Command command) 
            var result = SignInManager.PasswordSignInAsync(command.Username, command.Password, true, false).Result;

            return result;
        
    

SignOut.cs

public sealed class SignOut 
    public sealed class Command :
        IRequest 
    

    public sealed class CommandHandler :
        HandlerBase<Command> 
        private ApplicationSignInManager SignInManager  get; 

        public CommandHandler(
            DbContext context,
            ApplicationSignInManager signInManager)
            : base(context) 
            SignInManager = signInManager;
        

        protected override async void Handle(
            Command command) 
            await SignInManager.SignOutAsync();
        
    

这里有所有相关的代码,从我如何配置身份到我如何登录和退出。我仍然不明白为什么 Identity.External 在我从未要求过的情况下出现在画面中。

从技术上讲,SignInSignOut 类可以删除,它们的功能合并到 DefaultController 中,但是我选择保留它们以保持应用程序结构一致。

【问题讨论】:

见No authentication handler is configured to handle the scheme。 我遇到过那个帖子,但它对我没有帮助。我要注意的一件事是我使用SignInManager.PasswordSignInAsync 方法登录,因为我需要检查结果和SignInManager.SingOutAsync 用于注销。 您能否与我们分享完整的代码和详细步骤来重现您的问题? @Edward,我已根据您的要求使用相关代码更新了我的帖子。 【参考方案1】:

首先,我会避免扩展 ServiceCollection 类。相反,我会调用 AddIdetityCore 方法。查看源代码here。

然后:

services.AddIdentityCore<ApplicationUser>()
                .AddUserStore<UserStore>()
                .AddDefaultTokenProviders()
                .AddSignInManager<SignInManager<ApplicationUser>>();

其次,在 AddCookie 方法选项中设置 Events 属性。由于您没有为 ValidationInterval 属性设置时间段,因此它将持续 30 分钟。这意味着一旦时间结束,将在服务器发出的下一个请求中验证用户的 SecurityStamp 属性。由于在您所做的描述中您没有说您是否更改了密码,我怀疑用户的 SecurityStamp 在 BD 中为空,而它的 Cookie 版本是一个空字符串,所以当 Identity 在两个版本之间进行验证时(null = = "") 这将是错误的和then Identity would try to close the session of the Application Scheme, the Extern one and also the TwoFactor。然后它会抛出异常,因为只注册了ApplicationScheme:

public virtual async Task SignOutAsync()

    await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
    await Context.SignOutAsync(IdentityConstants.ExternalScheme); //<- Problem and...
    await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme); //... another problem.

解决方案首先是确保 SecurityStamp 不为空。然后你有两个选择:

Adding the cookies for every scheme

或者

从 SignInManager 类重写 SignOutAsync 方法。

public class SignInManager<TUser> : Microsoft.AspNetCore.Identity.SignInManager<TUser> 
    where TUser : class

    public SignInManager(
        UserManager<TUser> userManager, 
        IHttpContextAccessor contextAccessor, 
        IUserClaimsPrincipalFactory<TUser> claimsFactory, 
        IOptions<IdentityOptions> optionsAccessor, 
        ILogger<Microsoft.AspNetCore.Identity.SignInManager<TUser>> logger, 
        IAuthenticationSchemeProvider schemes) 
        : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
    
    

    public async override Task SignOutAsync()
    
        await Context.SignOutAsync(IdentityConstants.ApplicationScheme); // <- 
    

然后:

services.AddIdentityCore<ApplicationUser>()
                .AddUserStore<UserStore>()
                .AddDefaultTokenProviders()
                .AddSignInManager<Services.Infrastructure.Identity.SignInManager<ApplicationUser>>() //<-

【讨论】:

我已经忘记了这个问题,因为它已经很久了,但最终结果证明我选择不在数据库中存储安全标记。也许是因为我将成为唯一的用户并且没有多想。几个月后,我需要它来为一个真正的多用户项目工作,所以我对它进行了更多探索。我打开了一个 GitHub 问题,终于在一些帮助下弄清楚了:github.com/aspnet/Identity/issues/2082。现在我当前的设置有效,它可能与我上面发布的不同,但我会看看你的建议。谢谢!【参考方案2】:

最后结果证明我很笨,没有将安全标记存储在数据库中。不太清楚为什么这么久后我决定这样做。

由于@Shche 提醒了这篇文章的存在,我决定部分尝试他的推荐。

我已经将身份配置提取到一个扩展方法中,我将@Shche关于如何添加服务的建议纳入其中。这是扩展方法:

public static class IdentityExtensions 
    public static IServiceCollection AddApplicationIdentity(
        this IServiceCollection services) 
        services.AddAuthentication(
            o => 
                o.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
                o.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
                o.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
            ).AddCookie(IdentityConstants.ApplicationScheme,
            o => 
                o.Cookie.Expiration = TimeSpan.FromHours(8);
                o.Cookie.SameSite = SameSiteMode.Strict;
                o.Cookie.SecurePolicy = CookieSecurePolicy.Always;

                o.AccessDeniedPath = new PathString("/");
                o.ExpireTimeSpan = TimeSpan.FromHours(8);
                o.LoginPath = new PathString("/sign-in");
                o.LogoutPath = new PathString("/sign-out");
                o.SlidingExpiration = true;
            );

        services.AddIdentityCore<User>(
                    o => 
                        o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                        o.Lockout.MaxFailedAccessAttempts = 5;

                        o.Password.RequiredLength = 8;
                    )
                .AddSignInManager<ApplicationSignInManager>()
                .AddUserStore<ApplicationUserStore>();

        services.Configure<SecurityStampValidatorOptions>(
            o => 
                o.ValidationInterval = TimeSpan.FromMinutes(1);
            );

        return services;
    

【讨论】:

我决定改变主意,选择@Shche 的答案而不是我的答案。主要原因是因为我正在重新访问它,因为我想知道为什么我不能只指定注销方案而不覆盖SignOutAsync()。好吧,根据@Shche 的回答,这是因为SignOutAsync() 并不关心您声明使用的内容,它会退出所有使用或未使用的方案。这确实是我必须覆盖内置 SignInManager 的唯一方法的唯一原因。

以上是关于“Identity.External”的 ASP.NET Core 标识异常的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET和ASP的区别是什么

ASP.NET MVC

ASP与ASP.NET的区别

ASP的不足与ASP.NET和ASP的区别

ASP.NET和ASP的区别是啥?

ASP.NET Core与ASP.NET区别