Blazor 服务器和 CleanCode - ASP.NET 核心标识
Blazor Server and CleanCode - ASP.NET Core Identity
我无法将身份连接到具有 ASP.NET 核心身份的 Blazor 服务器。特别是在 Blazor 页面中获取正确的登录状态(当我从 Blazor 页面获取它们时)。
我认为这与在另一个项目中初始化的某些启动有关 - 但不确定如何调试它或解决方案是什么才能正确获得登录状态。
复制步骤和link下面的 GH 回购作为 POC。
背景
我正在将 clean-code project by JasonTaylor 从 Angular / ASP.NET Core 移植到具有 ASP.NET Core Identity 的 Blazor 服务器项目。
问题
应用程序运行起来,我可以在注册时浏览页面我可以在 identity-based 默认页面中看到 logged-in 状态,但在使用 AuthorizeView
的 Blazor 页面中(例如 LoginDisplay.razor
) 它不知道被授权。
在 Blazor 项目中启动:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication();
services.AddInfrastructure(Configuration);
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddSingleton<ICurrentUserService, CurrentUserService>();
services.AddHttpContextAccessor();
services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();
services.AddRazorPages();
services.AddServerSideBlazor();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/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.UseHealthChecks("/health");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
AddInfrastructure
在另一个项目中在启动时引用:
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("CleanArchitectureDb"));
}
else
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
}
services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
services.AddScoped<IDomainEventService, DomainEventService>();
services
.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddTransient<IDateTime, DateTimeService>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<ICsvFileBuilder, CsvFileBuilder>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddAuthorization(options =>
{
options.AddPolicy("CanPurge", policy => policy.RequireRole("Administrator"));
});
return services;
}
}
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
_options = optionsAccessor.Value;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user == null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
重现步骤
- 注册:https://localhost:44399/Identity/Account/Register
- 浏览到:https://localhost:44399/Identity/Account/Login - 注意 header 中的用户名是从 ASP.Net 身份页面
填充的
- 浏览到:https://localhost:44399/ - 注意 Header 是注册、登录、关于(基于 https://github.com/davidshorter/CleanCodeBlazor/blob/Rework/src/Web/Shared/LoginDisplay.razor)
如果有人喜欢的话,将我对 GH 的更改推送
看:https://github.com/davidshorter/CleanCodeBlazor/tree/Rework
这是混合使用 IdentityServer 和 ASP.net 身份的问题。
通过删除 Microsoft.AspNetCore.ApiAuthorization.IdentityServer
并使用基数 class 从 ApiAuthorizationDbContext<ApplicationUser>
回到 IdentityDbContext<ApplicationUser>
解决了这个问题。
我无法将身份连接到具有 ASP.NET 核心身份的 Blazor 服务器。特别是在 Blazor 页面中获取正确的登录状态(当我从 Blazor 页面获取它们时)。
我认为这与在另一个项目中初始化的某些启动有关 - 但不确定如何调试它或解决方案是什么才能正确获得登录状态。
复制步骤和link下面的 GH 回购作为 POC。
背景
我正在将 clean-code project by JasonTaylor 从 Angular / ASP.NET Core 移植到具有 ASP.NET Core Identity 的 Blazor 服务器项目。
问题
应用程序运行起来,我可以在注册时浏览页面我可以在 identity-based 默认页面中看到 logged-in 状态,但在使用 AuthorizeView
的 Blazor 页面中(例如 LoginDisplay.razor
) 它不知道被授权。
在 Blazor 项目中启动:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication();
services.AddInfrastructure(Configuration);
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddSingleton<ICurrentUserService, CurrentUserService>();
services.AddHttpContextAccessor();
services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();
services.AddRazorPages();
services.AddServerSideBlazor();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/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.UseHealthChecks("/health");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
AddInfrastructure
在另一个项目中在启动时引用:
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("CleanArchitectureDb"));
}
else
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
}
services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
services.AddScoped<IDomainEventService, DomainEventService>();
services
.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddTransient<IDateTime, DateTimeService>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<ICsvFileBuilder, CsvFileBuilder>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddAuthorization(options =>
{
options.AddPolicy("CanPurge", policy => policy.RequireRole("Administrator"));
});
return services;
}
}
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
_options = optionsAccessor.Value;
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user == null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
重现步骤
- 注册:https://localhost:44399/Identity/Account/Register
- 浏览到:https://localhost:44399/Identity/Account/Login - 注意 header 中的用户名是从 ASP.Net 身份页面 填充的
- 浏览到:https://localhost:44399/ - 注意 Header 是注册、登录、关于(基于 https://github.com/davidshorter/CleanCodeBlazor/blob/Rework/src/Web/Shared/LoginDisplay.razor)
如果有人喜欢的话,将我对 GH 的更改推送 看:https://github.com/davidshorter/CleanCodeBlazor/tree/Rework
这是混合使用 IdentityServer 和 ASP.net 身份的问题。
通过删除 Microsoft.AspNetCore.ApiAuthorization.IdentityServer
并使用基数 class 从 ApiAuthorizationDbContext<ApplicationUser>
回到 IdentityDbContext<ApplicationUser>
解决了这个问题。