У меня есть решение в .NET 7, которое работает нормально, однако я хочу перейти на .NET 8 с новой структурой, представленной MS, и сталкиваюсь с множеством проблем, поскольку аутентификация работает не так, как мне хотелось бы.
Итак, в AuthService у меня есть логика для получения данных из AD. Я получаю имя, фамилию, адрес электронной почты и отдел на основе имени пользователя, которое я получаю в CustomServerAuthenticationStateProvider.GetAuthenticationStateAsync. При таком подходе я получаю SSO.
Если пользователь еще не был на сайте, я регистрирую его и создаю запись в базе данных, где позже могу назначить ему соответствующие роли, необходимые для его работы. Если его имя пользователя уже найдено в базе данных, я просто читаю его роли и присваиваю их ClaimsIdentity -> userWinIdentity.AddClaims(_userService.GetRolesClaims());
Итак, я перенес все на новое решение .NET8, и оно работает, если я перехожу на страницу, щелкая ссылку в главном меню, которая переводит пользователя на страницу Profile.razor. Когда я там и нажимаю F5, чтобы обновить страницу, я получаю эту ошибку:
Access to host localhost is denied.
You do not have user rights to view this page.
HTTP 403 ERROR
Программа.cs
using BlazorServer.Data;
using BlazorServer.Providers;
using BlazorServer.Services;
using BlazorServer.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using BlazorServer.Consts;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddControllers();
builder.Services.AddLocalization();
builder.Services.AddMudServices();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<RoleService>();
builder.Services.AddScoped<TimeZoneService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomServerAuthenticationStateProvider>();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// 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.UseHttpsRedirection();
app.UseRouting();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapControllers();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(CultureConsts.supportedCultures[0])
.AddSupportedCultures(CultureConsts.supportedCultures)
.AddSupportedUICultures(CultureConsts.supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.Run();
CustomServerAuthenticationStateProvider.cs
public class CustomServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
{
private readonly AuthService _authService;
private readonly UserService _userService;
private readonly IHttpContextAccessor _httpContextAccessor;
private Task<AuthenticationState> _authenticationStateTask;
public CustomServerAuthenticationStateProvider(AuthService authService, UserService userService, IHttpContextAccessor httpContextAccessor)
{
_authService = authService;
_userService = userService;
_httpContextAccessor = httpContextAccessor;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
var userWinIdentity = (ClaimsIdentity)httpContext?.User.Identity!;
if (userWinIdentity != null && userWinIdentity.IsAuthenticated && !string.IsNullOrEmpty(userWinIdentity.Name))
{
var username = userWinIdentity.Name.Split('\\').Last();
User userObj = _authService.Authenticate(username);
_userService.User = userObj;
userWinIdentity.AddClaims(_userService.AddCustomUserClaims());
userWinIdentity.AddClaims(_userService.GetRolesClaims());
}
var user = new ClaimsPrincipal(userWinIdentity);
return await Task.FromResult(new AuthenticationState(user));
}
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
{
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
NotifyAuthenticationStateChanged(_authenticationStateTask);
}
}
Пользовательская служба.cs
public List<Claim> AddCustomUserClaims()
{
return
[
new Claim("FullName", User.FullName)
];
}
public List<Claim> GetRolesClaims()
{
List<Claim> claims = new();
foreach (var role in User.Roles)
claims.Add(new Claim(ClaimTypes.GroupSid, role.Name));
// if I use ClaimTypes.Role then it does not work properly on Profile.razor, I do not know why. If someone has a clue please let me know
return claims;
}
Профиль.бритва
@page "/profile"
@attribute [Authorize(Roles = $"{SecurityGroup.RegisteredUser}")]
@inject UserService userService
@rendermode RenderMode.InteractiveServer
<PageTitle>@localizer["Info portal"] | @localizer["Profile"]</PageTitle>
<AuthorizeView>
@if (user != null)
{
<div class = "container-fluid">
<div class = "row">
<div class = "col-xs-3 col-sm-3 col-md-3 col-lg-2" style = "font-weight: bold;">
@localizer["Username"]
</div>
<div class = "col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.Username
</div>
</div>
<div class = "row">
<div class = "col-xs-3 col-sm-3 col-md-3 col-lg-2" style = "font-weight: bold;">
@localizer["First name"]
</div>
<div class = "col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.FirstName
</div>
</div>
<div class = "row">
<div class = "col-xs-3 col-sm-3 col-md-3 col-lg-2" style = "font-weight: bold;">
@localizer["Last name"]
</div>
<div class = "col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.LastName
</div>
</div>
<div class = "row">
<div class = "col-xs-3 col-sm-3 col-md-3 col-lg-2" style = "font-weight: bold;">
@localizer["Email"]
</div>
<div class = "col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.Email
</div>
</div>
<div class = "row">
<div class = "col-xs-3 col-sm-3 col-md-3 col-lg-2" style = "font-weight: bold;">
@localizer["Department"]
</div>
<div class = "col-xs-9 col-sm-9 col-md-9 col-lg-10">
@user.Department
</div>
</div>
</div>
}
</AuthorizeView>
@code {
User user = new();
protected override async Task OnInitializedAsync()
{
user = userService.User;
}
}
Пользователь.cs
public class User
{
public User()
{
Roles = new HashSet<Role>();
}
[Key]
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PersonalNr { get; set; } = string.Empty;
public virtual ICollection<Role> Roles { get; set; }
}
Я установил следующие свойства, чтобы иметь возможность получить пользователя, вошедшего в систему Windows: Дважды щелкните проект:
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
**<UseWindowsService>true</UseWindowsService>**
Щелкните правой кнопкой мыши проект -> свойства -> отладка -> открыть профили запуска отладки -> Включить проверку подлинности Windows.
Проблема в том, что CustomServerAuthenticationStateProvider не запускается при обновлении страницы. Я уже везде проверял и проверял множество разных подходов, но ни один из них не работает, поэтому я не могу понять, что мне нужно сделать, чтобы решить эту проблему.
Есть еще одна загадка, на которую я не знаю ответа. В UserService у меня есть метод добавления утверждений на основе ролей, определенных пользователем в базе данных. Если я добавлю их вот так:claims.Add(new Claim(ClaimTypes.GroupSid, role.Name)); тогда авторизация работает в продакшене.
Если я подделаю логин на своей машине разработки следующим образом:
userWinIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "username"),
}, "Fake authentication type");
затем мне нужно обновить код в UserService и использовать его следующим образом:claims.Add(new Claim(ClaimTypes.Role, role.Name));
Это похоже на .NET 7. Если кто-нибудь знает, почему он ведет себя по-разному при разработке и производстве, дайте мне знать.
Поэтому любая помощь будет принята с благодарностью.
Обновлено: Я поигрался и понял, что из того, что я получил из шаблона, я изменил Routes.razor, по сути добавил, хотя он и не нужен. Если я удалю этот фрагмент кода, мой код даже не будет работать.
<CascadingAuthenticationState>
<Router AppAssembly = "typeof(Program).Assembly">
<Found Context = "routeData">
<RouteView RouteData = "routeData" DefaultLayout = "typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData = "routeData" Selector = "h1" />
</Found>
</Router>
В этом случае я получаю эту ошибку:
An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an object.
InfoPortal.Components.Layout.UserButton.<BuildRenderTree>b__0_5(RenderTreeBuilder __builder2)
Stack Query Cookies Headers Routing
NullReferenceException: Object reference not set to an instance of an object.
InfoPortal.Components.Layout.UserButton.<BuildRenderTree>b__0_5(RenderTreeBuilder __builder2)
Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddContent(int sequence, RenderFragment fragment)
Microsoft.AspNetCore.Components.Authorization.AuthorizeViewCore.BuildRenderTree(RenderTreeBuilder builder)
Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, out Exception renderFragmentException)
Microsoft.AspNetCore.Components.RenderTree.Renderer.HandleExceptionViaErrorBoundary(Exception error, ComponentState errorSourceOrNull)
Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessPendingRender()
Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToRenderQueue(int componentId, RenderFragment renderFragment)
Microsoft.AspNetCore.Components.RenderHandle.Render(RenderFragment renderFragment)
Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()
Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
Microsoft.AspNetCore.Components.RenderTree.Renderer.HandleExceptionViaErrorBoundary(Exception error, ComponentState errorSourceOrNull)
Microsoft.AspNetCore.Components.RenderTree.Renderer.AddToPendingTasksWithErrorHandling(Task task, ComponentState owningComponentState)
Microsoft.AspNetCore.Components.Rendering.ComponentState.SupplyCombinedParameters(ParameterView directAndCascadingParameters)
Microsoft.AspNetCore.Components.Rendering.ComponentState.SetDirectParameters(ParameterView parameters)
Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(int componentId, ParameterView initialParameters)
Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(IComponent component, ParameterView initialParameters)
Microsoft.AspNetCore.Components.Endpoints.EndpointHtmlRenderer.RenderEndpointComponent(HttpContext httpContext, Type rootComponentType, ParameterView parameters, bool waitForQuiescence)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
Microsoft.AspNetCore.Components.Endpoints.RazorComponentEndpointInvoker.RenderComponentCore(HttpContext context)
Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext+<>c+<<InvokeAsync>b__10_0>d.MoveNext()
Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Обновлено еще раз: Пример проекта https://easyupload.io/5oejk7
@FengzhiZhou Я добавил ссылку на образец проекта в первый пост. Спасибо, что заглянули.
@FengzhiZhou Вам нужно только создать файл миграции. Я использовал существующую базу данных, поэтому забыл ее добавить.





Мне удалось решить проблему с помощью одного из решений, предложенных по этой ссылке. https://www.codeproject.com/Questions/5362704/Blazor-windows-authentication-with-custom-claims
Решение, которое я использовал:
program.cs должен указывать на собственный аутентификатор с настраиваемой схемой ->
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "CustomWindowsAuthentication";
}).AddScheme<CustomWindowsAuthenticationOptions, CustomWindowsAuthenticationHandler>("CustomWindowsAuthentication", null);
Тогда вам действительно нужен дополнительный класс, который обрабатывает эти вещи:
public class CustomWindowsAuthenticationHandler : AuthenticationHandler<CustomWindowsAuthenticationOptions>
{
public CustomWindowsAuthenticationHandler(
IOptionsMonitor<CustomWindowsAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.User.Identity.IsAuthenticated || !(Context.User.Identity is WindowsIdentity windowsIdentity))
{
return AuthenticateResult.NoResult();
}
var loginName = windowsIdentity.Name;
if (loginName.Contains("User1")
|| loginName.Contains("User2")
|| loginName.Contains("User3")
|| loginName.Contains("User4"))
{
var claims = new List<Claim>
{
new Claim("CustomClaim", "Admin")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
return AuthenticateResult.Fail("Custom authentication failed.");
}
}
И для полноты картины «CustomWindowsAuthenticationOptions», потому что вам это тоже нужно, хотя оно пустое, так как мне не нужны какие-то особые опции.
public class CustomWindowsAuthenticationOptions : AuthenticationSchemeOptions
{
}
Требуется настройка вашей производственной среды, чтобы проверить причину различного поведения ваших утверждений. В сообщении об ошибке понятно, что у какого-то объекта нет экземпляра, а это значит, что вам нужно проверить, не зарегистрирован ли какой-то сервис правильно или у некоторых объектов нет экземпляра. Я не могу воспроизвести проблему локально, так как не хватает некоторого кода. Будет лучше, если вы предоставите минимальный воспроизводимый образец кода.