核心 5,PasswordSignInAsync 无法在双重身份验证方案中设置 cookie

Posted

技术标签:

【中文标题】核心 5,PasswordSignInAsync 无法在双重身份验证方案中设置 cookie【英文标题】:Core 5, PasswordSignInAsync fails to set cookie in dual Authentication scheme 【发布时间】:2022-01-10 18:23:19 【问题描述】:

我正在编写一个前端/后端应用程序。前端是一个 Angular 13 应用程序。后端是后端 API 和管理网站的组合。后端有:

本地身份(包括身份脚手架), Web API(用于使用 Swagger 不记名令牌的 Angular 前端), 用于边桌管理的 MVC 视图/控制器。

前端需要访问 API 服务。登录返回一个令牌。令牌用于访问各种服务以维护应用程序表。

后端 .net 5 Core 网站读取和写入本地 SQL Server 数据库。该数据库包含身份表。后端还用于使用脚手架 Razor 页面维护身份表。后端为许多管理表维护(基本 CRUD)。因此,用户或管理员使用与 Angular 前端相同的登录帐户通过脚手架登录表单登录。

问题是通过 PasswordSignInAsync 登录成功,但 User.Identity.IsAuthenticated 为假。我在很多地方都使用 User.Identity 来表示名称、角色和 IsAuthenticated。我感觉 User.Identity 应该在使用 cookie 身份验证方案时自动发生。我添加了双重方案,但这并没有解决问题。我已经阅读了每个 PasswordSignInAsync 不起作用的许多问题,但似乎没有任何帮助。我试图解决问题的事情可能会对结果产生不利影响。我已阅读 CheckPasswordSignInAsync 的源代码,但没有看到 User.Identity 的任何设置。不知道该怎么做才能解决这个问题。

请随时要求任何澄清。

我正在展示我的完整 Startup.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using System;
using System.Threading.Tasks;
//
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using FluentValidation.AspNetCore;
using MediatR;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using NSG.NetIncident4.Core.Domain.Entities.Authentication;
using NSG.NetIncident4.Core.Infrastructure.Authentication;
using NSG.NetIncident4.Core.Infrastructure.Common;
using NSG.NetIncident4.Core.Infrastructure.Notification;
using NSG.NetIncident4.Core.Infrastructure.Services;
//
namespace NSG.NetIncident4.Core

  public class Startup
  
    public Startup(IConfiguration configuration)
    
      Configuration = configuration;
    

    public IConfiguration Configuration  get; 

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    
      //
      services.AddControllers();
      /*
      ** Configure logging
      */
      services.AddLogging(builder => builder
        .AddConfiguration(Configuration.GetSection("Logging"))
        .AddConsole()
        .AddDebug()
      );
      /*
      ** Cookie options.
      */
      #region Cookie options
      services.Configure<CookiePolicyOptions>(options =>
      
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
      );
      services.ConfigureApplicationCookie(options =>
      
        // Cookie settings
        options.Cookie.HttpOnly = true;
        options.Cookie.Name = "Net-Incident.Identity";
        options.LoginPath = new PathString("/Account/Login");
        options.LogoutPath = new PathString("/Account/Logout");
        options.AccessDeniedPath = new PathString("/Account/AccessDenied");
        options.SlidingExpiration = true;
        options.ExpireTimeSpan = TimeSpan.FromHours(2);
      );
      #endregion
      /*
      ** Read values from the 'appsettings.json'
      ** * Add and configure email/notification services
      ** * Services like ping/whois
      ** * Applications information line name and phone #
      ** * Various authorization information
      */
      services.Configure<MimeKit.NSG.EmailSettings>(Configuration.GetSection("EmailSettings"));
      services.Configure<ServicesSettings>(Configuration.GetSection("ServicesSettings"));
      services.Configure<ApplicationSettings>(Configuration.GetSection("ApplicationSettings"));
      AuthSettings _authSettings = Options.Create<AuthSettings>(
        Configuration.GetSection("AuthSettings").Get<AuthSettings>()).Value;
      services.Configure<AuthSettings>(Configuration.GetSection("AuthSettings"));
      // call local method below
      ConfigureDatabase(services);
      /*
      ** For Identity
      */
      services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddDefaultUI()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
      /*
      ** Add CORS
      */
      services.AddCors(options => 
        options.AddPolicy("CorsAnyOrigin", builder => 
          builder
            .WithOrigins("http://localhost:4200,http://localhost:10111")
            .AllowAnyOrigin()
            .AllowAnyHeader()
            .AllowAnyMethod();
        );
      );
      /*
      ** Add Authentication
      */
      #region Add Authentication
      services.AddAuthentication(options =>
      
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = "BearerOrCookie";
      )
        /*
        ** Add Jwt Bearer
        */
        .AddJwtBearer(options =>
        
          options.SaveToken = true;
          options.RequireHttpsMetadata = false;
          options.TokenValidationParameters = new TokenValidationParameters()
          
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidAudience = Configuration["JWT:ValidAudience"],
            ValidIssuer = Configuration["JWT:ValidIssuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
          ;
        )
        /*
        ** Add cookie authentication scheme
        */
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        /*
        ** Conditionally add either JWT bearer of cookie authentication scheme
        */
        .AddPolicyScheme("BearerOrCookie", "Custom JWT bearer or cookie", options =>
        
          options.ForwardDefaultSelector = context =>
          
            // since all my api will be starting with /api, modify this condition as per your need.
            if (context.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
              return JwtBearerDefaults.AuthenticationScheme;
            else
              return CookieAuthenticationDefaults.AuthenticationScheme;
          ;
        );
      #endregion // Add Authentication
      /*
      ** Add authorization services
      */
      services.AddAuthorization(options =>
      
        options.AddPolicy("AdminRole", policy => policy.RequireRole("Admin"));
        options.AddPolicy("CompanyAdminRole", policy => policy.RequireRole("Admin", "CompanyAdmin"));
        options.AddPolicy("AnyUserRole", policy => policy.RequireRole("User", "Admin", "CompanyAdmin"));
        Console.WriteLine(options);
      );
      /*
      ** Add Swagger services
      */
      #region Add Swagger
      services.AddSwaggerGen(swagger =>
      
        // Generate the Default UI of Swagger Documentation
        swagger.SwaggerDoc("v1", new OpenApiInfo
        
          Version = "v1",
          Title = "NSG Net-Incident4.Core",
          Description = "Authentication and Authorization in ASP.NET 5 with JWT and Swagger"
        );
        // To Enable authorization using Swagger (JWT)
        swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
        
          Name = "Authorization",
          Type = SecuritySchemeType.ApiKey,
          Scheme = "Bearer",
          BearerFormat = "JWT",
          In = ParameterLocation.Header,
          Description = "Enter 'Bearer' [space] and then your valid token in the text input below.\r\n\r\nExample: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
        );
        swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
        
          
            new OpenApiSecurityScheme
            
              Reference = new OpenApiReference
              
                Type = ReferenceType.SecurityScheme,
                Id = "Bearer"
              
            ,
            new string[] 
          
        );
      );
      #endregion // end of AddSwagger
      /*
      ** Add email/notification services
      */
      services.AddSingleton<INotificationService, NotificationService>();
      services.AddSingleton<IEmailSender, NotificationService>();
      // Add framework services.
      services.AddTransient<IApplication, ApplicationImplementation>();
      /*
      ** Configure MVC service.
      */
      services.AddMvc(option => option.EnableEndpointRouting = false)
        .AddFeatureFolders()
        .AddAreaFeatureFolders()
        .AddApplicationPart(typeof(Startup).Assembly)
        .AddSessionStateTempDataProvider()
        .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>())
        .AddRazorPagesOptions(options =>
        
          options.Conventions.Clear();
          options.RootDirectory = "/UI/Identity";
        );
      /*
      ** Configure locations of views
      */
      #region View locations
      services.Configure<RazorViewEngineOptions>(options =>
      
        // https://github.com/OdeToCode/AddFeatureFolders
        // 2 is area, 1 is controller,0 is the action
        options.ViewLocationFormats.Clear();
        options.ViewLocationFormats.Add("/UI/Views/1/0" + RazorViewEngine.ViewExtension);
        options.ViewLocationFormats.Add("/UI/Views/Admin/1/0" + RazorViewEngine.ViewExtension);
        options.ViewLocationFormats.Add("/UI/Views/CompanyAdmin/1/0" + RazorViewEngine.ViewExtension);
        options.ViewLocationFormats.Add("/UI/Views/Shared/0" + RazorViewEngine.ViewExtension);
        // now razor pages
        options.PageViewLocationFormats.Clear();
        options.PageViewLocationFormats.Add("/UI/Views/1/0" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Views/Admin/1/0" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Views/CompanyAdmin/1/0" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Views/Shared/0" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Identity/0" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Identity/Account/0" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Identity/Account/Manage/0" + RazorViewEngine.ViewExtension);
        //
        options.AreaViewLocationFormats.Clear();
        options.AreaViewLocationFormats.Add("/UI/Views/1/0" + RazorViewEngine.ViewExtension);
        options.AreaViewLocationFormats.Add("/UI/Views/Admin/1/0" + RazorViewEngine.ViewExtension);
        options.AreaViewLocationFormats.Add("/UI/Views/CompanyAdmin/1/0" + RazorViewEngine.ViewExtension);
        options.AreaViewLocationFormats.Add("/UI/Views/Shared/0" + RazorViewEngine.ViewExtension);
        // now razor areas
        options.AreaPageViewLocationFormats.Clear();
        options.AreaPageViewLocationFormats.Add("/UI/Views/1/0" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Views/Admin/1/0" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Views/CompanyAdmin/1/0" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Views/Shared/0" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Identity/Account/Manage/0" + RazorViewEngine.ViewExtension);
      );
      #endregion // View locations
      /*
      ** Add session for state for temp data provider
      */
      services.AddSession(options =>
      
        options.Cookie.IsEssential = true;
      );
      //
    
    //
    /// <summary>
    /// An overridable method, that allows for different configuration.
    /// </summary>
    /// <param name="services">The current collection of services, 
    /// add DB contect to the container.
    /// </param>
    public virtual void ConfigureDatabase(IServiceCollection services)
    
      string _connetionString = Configuration.GetConnectionString("DefaultConnection");
      if (string.IsNullOrEmpty(_connetionString))
      
        throw (new ApplicationException("No connection string found"));
      
      services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(_connetionString));
    
    //
    /// <summary>
    /// This method gets called by the runtime.
    /// Use this method to configure the HTTP request pipeline.
    /// </summary>
    public void Configure(
      IApplicationBuilder app,
      IWebHostEnvironment env,
      ApplicationDbContext context,
      UserManager<ApplicationUser> userManager,
      RoleManager<ApplicationRole> roleManager)
    
      if (env.IsDevelopment())
      
        app.UseDeveloperExceptionPage();
      
      else
      
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
      
      //
      app.UseHttpsRedirection();
      app.UseStaticFiles();
      app.UseRouting();
      // routing/CORS/endpoint
      app.UseCors("CorsAnyOrigin");
      app.UseAuthentication();
      app.UseAuthorization();
      app.UseCookiePolicy();
      app.UseSession();
      app.UseSwagger();
      // app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NSG Net-Incident4.Core v1"));
      app.UseMvc(routes =>
      
        routes.MapRoute(
          name: "default",
          template: "controller=Home/action=Index/id?");
      );
      //
    
  

login.chtml.cs 的一个 sn-p:

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    
      returnUrl ??= Url.Content("~/");
      ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
      if (ModelState.IsValid)
      
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        
          _logger.LogInformation($"User: Input.UserName is logged in.");
          //
          var _user = await _signInManager.UserManager.FindByNameAsync(Input.UserName);
          ClaimsPrincipal _userPrincipal = await _signInManager.CreateUserPrincipalAsync(_user);
          await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, _userPrincipal);
          //
          return LocalRedirect(returnUrl);
        
        if (result.RequiresTwoFactor)
        
          return RedirectToPage("./LoginWith2fa", new  ReturnUrl = returnUrl, RememberMe = Input.RememberMe );
        
        if (result.IsLockedOut)
        
          _logger.LogWarning("User account locked out.");
          return RedirectToPage("./Lockout");
        
        else
        
          ModelState.AddModelError(string.Empty, "Invalid login attempt.");
          return Page();
        
      
      // If we got this far, something failed, redisplay form
      return Page();
    

【问题讨论】:

【参考方案1】:

从零开始后,我觉得我找到了问题所在。我现在通过 Swagger API 服务(Angular 13)和 logon.cshtml Identity Razor 脚手架页面登录。

正确添加 Identity 脚手架时,需要更改/查看 IdentityHostingStartup.cs 文件。我更新的 IdentityHostingStartup 如下:

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NSG.NetIncident4.Core.Domain.Entities.Authentication;

[assembly: HostingStartup(typeof(NSG.NetIncident4.Core.UI.Identity.IdentityHostingStartup))]
namespace NSG.NetIncident4.Core.UI.Identity

    public class IdentityHostingStartup : IHostingStartup
    
        public void Configure(IWebHostBuilder builder)
        
            builder.ConfigureServices((context, services) => 
                //
                string _connetionString = context.Configuration.GetConnectionString("DefaultConnection");
                if (string.IsNullOrEmpty(_connetionString))
                
                    throw (new ApplicationException("No connection string found"));
                
                services.AddDbContext<ApplicationDbContext>(options =>
                    options.UseSqlServer(_connetionString));
                services.AddDefaultIdentity<ApplicationUser>(options => 
                        options.SignIn.RequireConfirmedAccount = true;
                        options.Password.RequireDigit = true;
                        options.Password.RequiredLength = 8;
                        options.Password.RequireLowercase = true;
                        options.Password.RequireUppercase = true;
                        options.Password.RequireNonAlphanumeric = true;
                    )
                    .AddRoles<ApplicationRole>()
                    .AddEntityFrameworkStores<ApplicationDbContext>();
                //
            );
        
    

这使得 Startup.cs 文件更加简单。 Startup添加以下服务:

日志记录, 带有视图的控制器, 查看位置, 通知, 授权政策, 会话, CORS, 大摇大摆。

配置/使用方法保持不变。

【讨论】:

以上是关于核心 5,PasswordSignInAsync 无法在双重身份验证方案中设置 cookie的主要内容,如果未能解决你的问题,请参考以下文章

对 signInManager.PasswordSignInAsync 进行故障排除 [重复]

Identity 3 SignInManager.PasswordSignInAsync() 不返回任何结果

SignInManager.PasswordSignInAsync() 成功,但 User.Identity.IsAuthenticated 为 false

将自定义SignInResults添加到从SignInManager返回的内容 PasswordSignInAsync

关闭浏览器 ASP.NET MVC 后如何注销用户

asp.net core 5 mvc 超级管理员连接