使用 .AddIdentityServerJwt() 时,.NET Core Razor Pages 应用程序的身份验证不适用于没有“/Identity”路由的视图

Authentication for .NET Core Razor Pages application doesn't work for views without an "/Identity" route while using .AddIdentityServerJwt()

使用 .NET Core 3.1 框架,我正在尝试使用以下设置配置 Web 平台:

所有这三个组件 (Razor Pages + Angular + Identity Server) 都捆绑到一个 .NET Core Web 项目中。我还搭建了 Identity 脚手架,以便能够自定义页面的外观和行为。

我几乎可以按照我想要的方式配置应用程序,主要是混合使用 Razor Pages 选项(用户帐户存储在本地)和 Angular 模板选项 (用户帐户存储在本地)并经过一些反复试验和调查。

我申请的当前状态是:

  1. 用户登录 Razor Pages 应用程序。
  2. 登录成功,邮箱显示在导航栏
  3. 当我们导航到 SPA 时,我的 Angular 应用程序尝试静默登录并成功:

localhost:5001/Dashboard (Angular SPA home route)

  1. 如果我们导航到没有 /Identity 路由的 Razor Pages 应用程序的一部分(仅用于带有 Identity 的页面),cookie 似乎不再包含正确的信息我在这些路线上没有会话。这意味着,例如,如果我使用 SignInManager.IsSignedIn(User) 仅显示受 options.Conventions.AuthorizePage($"/Administration") 保护的管理页面的导航选项,如果我在具有 URL 的 URL 中Identity路由,会显示导航标签,否则不显示:

localhost:5001/Identity/Account/Login

localhost:5001(Razor Pages 应用程序主路径)

  1. 但是,即使当我在具有 /Identity 路由的 URL 上时显示管理导航选项卡,如果我单击它,我将收到 401 未经授权的错误,因为管理页面前面没有 /Identity 路由:

localhost:5001/管理

我已经设法将问题追溯到 AddIdentityServerJwt()。如果没有这个,Razor Pages 应用程序的登录将按预期工作,但我显然无法在之后对 Angular 应用程序使用身份验证和授权。

我去检查了 source code 的那个方法,结果发现它创建了一个新的 IdentityServerJwtPolicySchemeForwardSelector 将 JWT 策略方案转发给 DefaultIdentityUIPathPrefix,正如你可能猜对了,只包含值"/Identity".

我已按以下方式配置我的启动 class:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    services.Configure<CookiePolicyOptions>(options =>
    {            
        options.CheckConsentNeeded = context => true;            
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });
    services
        .AddDbContext<ApplicationDbContext>(optionsBuilder =>
        {
            DatabaseProviderFactory
                    .CreateDatabaseProvider(configuration, optionsBuilder);
        });
    services
        .AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services
        .AddIdentityServer()
        .AddApiAuthorization<IdentityUser, ApplicationDbContext>();
    services
        .AddAuthentication()
        .AddIdentityServerJwt();
    services
        .AddControllersWithViews();
    services
            .AddRazorPages()
            .AddRazorPagesOptions(options =>
            {
                options.Conventions.AuthorizePage($"/Administration");
            });       
    services
        .AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "ClientApp/dist";
        });

    services.AddTransient<IEmailSender, EmailSenderService>();
    services.Configure<AuthMessageSenderOptions>(configuration);

    services.AddTransient<IProfileService, ProfileService>();
}

public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment)
{
    SeedData.SeedDatabase(applicationBuilder, configuration);

    if (webHostEnvironment.IsDevelopment())
    {
        applicationBuilder.UseDeveloperExceptionPage();
        applicationBuilder.UseDatabaseErrorPage();
    }
    else
    {
        applicationBuilder.UseExceptionHandler("/Error");           
        applicationBuilder.UseHsts();
    }

    applicationBuilder.UseHttpsRedirection();
    applicationBuilder.UseStaticFiles();
    applicationBuilder.UseCookiePolicy();

    if (!webHostEnvironment.IsDevelopment())
    {
        applicationBuilder.UseSpaStaticFiles();
    }

    applicationBuilder.UseRouting();

    applicationBuilder.UseAuthentication();
    applicationBuilder.UseIdentityServer();
    applicationBuilder.UseAuthorization();
    applicationBuilder.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });

    applicationBuilder.UseSpa(spa =>
    {              
        spa.Options.SourcePath = "ClientApp";

        if (webHostEnvironment.IsDevelopment())
        {
            if (bool.Parse(configuration["DevelopmentConfigurations:UseProxyToSpaDevelopmentServer"]))
            {
                spa.UseProxyToSpaDevelopmentServer(configuration["DevelopmentConfigurations:ProxyToSpaDevelopmentServerAddress"]);
            }
            else
            {
                spa.UseAngularCliServer(npmScript: configuration["DevelopmentConfigurations:AngularCliServerNpmScript"]);
            }
        }
    });
}

如何配置我的应用程序,以便会话在我的整个应用程序中可用,而不仅仅是在具有“/Identity”路由的 URLs 上可用,同时保持身份验证和授权对于 Razor Pages 应用程序和 Angular 应用程序?

我遇到了同样的问题并通过添加我自己的 PolicyScheme 来解决它,它根据请求路径决定应该使用哪种类型的身份验证。我所有的剃刀页面都有一个以“/Identity”或“/Server”开头的路径,所有其他请求都应该使用 JWT。

我使用以下代码在 ConfigureServices 中进行设置:

// Add authentication using JWT and add a policy scheme to decide which type of authentication should be used
services.AddAuthentication()
    .AddIdentityServerJwt()
    .AddPolicyScheme("ApplicationDefinedAuthentication", null, options =>
    {
        options.ForwardDefaultSelector = (context) =>
        {
            if (context.Request.Path.StartsWithSegments(new PathString("/Identity"), StringComparison.OrdinalIgnoreCase) ||
                context.Request.Path.StartsWithSegments(new PathString("/Server"), StringComparison.OrdinalIgnoreCase))
                return IdentityConstants.ApplicationScheme;
            else
                return IdentityServerJwtConstants.IdentityServerJwtBearerScheme;
        };
    });

// Use own policy scheme instead of default policy scheme that was set in method AddIdentityServerJwt 
services.Configure<AuthenticationOptions>(options => options.DefaultScheme = "ApplicationDefinedAuthentication");