.NET 8 Blazor Server — авторизация ролей с проверкой подлинности Windows

У меня есть решение в .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

Требуется настройка вашей производственной среды, чтобы проверить причину различного поведения ваших утверждений. В сообщении об ошибке понятно, что у какого-то объекта нет экземпляра, а это значит, что вам нужно проверить, не зарегистрирован ли какой-то сервис правильно или у некоторых объектов нет экземпляра. Я не могу воспроизвести проблему локально, так как не хватает некоторого кода. Будет лучше, если вы предоставите минимальный воспроизводимый образец кода.

Fengzhi Zhou 18.06.2024 11:40

@FengzhiZhou Я добавил ссылку на образец проекта в первый пост. Спасибо, что заглянули.

mmaestro 19.06.2024 10:15

@FengzhiZhou Вам нужно только создать файл миграции. Я использовал существующую базу данных, поэтому забыл ее добавить.

mmaestro 19.06.2024 10:45
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
231
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мне удалось решить проблему с помощью одного из решений, предложенных по этой ссылке. 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
{
    
}

Другие вопросы по теме