У меня есть серверное приложение Blazor, которое требует от пользователей входа в систему с учетной записью Microsoft (в клиенте моей компании). Я пытаюсь обеспечить его дополнительную безопасность, разрешая только пользователям, которые соответствуют определенным критериям, но моя попытка сделать это делает приложение полностью неработоспособным в рабочей среде.Причина проблемы
Способ, которым я реализовал авторизацию (полная разница в конце вопроса), был следующим:
Создал класс DoingsAuthzMiddleware, который добавит удостоверение, включая утверждение DoingsAuthz, проверенным пользователям.
Добавлен builder.Services.AddAuthorization(opts => opts.AddPolicy("DoingsAuthz", apBuilder => apBuilder.RequireClaim("DoingsAuthz"))); и app.UseMiddleware(); в мой Program.cs.
Добавлен @attribute [Authorize(Policy = "DoingsAuthz")] в _Imports.razor для защиты всех страниц по умолчанию.
Добавлен @attribute [AllowAnonymous] на страницы, которые должны разрешать анонимный доступ (например, страницу 403).
Мне также пришлось внести некоторые изменения в мой Routes.razor, чтобы все состояние каскадной аутентификации работало, и коммит включает пару новых представлений для сценария 403.
Симптомы
Когда я запускаю локально с помощью dotnet run или любым другим способом, сайт работает так, как ожидалось. Оно допускает авторизованных пользователей, отклоняет неавторизованных и, по-видимому, не имеет других побочных эффектов.
В рабочей среде приложение развертывается в веб-приложении Azure с использованием бесплатного номера SKU F1 и работает под управлением Windows со стеком среды выполнения dotnet v9.0. Когда я развернул этот и 18 других коммитов, при посещении главной страницы я столкнулся с ошибкой HTTP 500.
Расследование
Вперед и назад
Сразу после неудачного развертывания я повторно развернул последний известный стабильный коммит. Это сработало, как и ожидалось. Затем я снова попытался развернуть плохую версию, чтобы исключить ошибку в этом конкретном развертывании. Я испытал те же симптомы, что и раньше. Затем я выполнил биссектрису, определив, что описанный выше коммит является нарушающим.
web.config
Используя az webapp logtail и нажав URL-адрес корневой страницы, я получил что-то похожее на HTML-страницу. Убрав весь ненужный HTML, мы получили вот такой контент:
HTTP Error 500.0 - Internal Server Error
The page cannot be displayed because an internal server error has occurred.
Most likely causes:
- IIS received the request; however, an internal error occurred during the processing of the request. The root cause of this error depends on which module handles the request and what was happening in the worker process when this error occurred.
- IIS was not able to access the web.config file for the Web site or application. This can occur if the NTFS permissions are set incorrectly.
- IIS was not able to process configuration for the Web site or application.
- The authenticated user does not have permission to use this DLL.
- The request is mapped to a managed handler but the .NET Extensibility Feature is not installed.
Things you can try:
- Ensure that the NTFS permissions for the web.config file are correct and allow access to the Web server's machine account.
- Check the event logs to see if any additional information was logged.
- Verify the permissions for the DLL.
- Install the .NET Extensibility feature if the request is mapped to a managed handler.
- Create a tracing rule to track failed requests for this HTTP status code. For more information about creating a tracing rule for failed requests, go to http://go.microsoft.com/fwlink/?LinkID=66439.
Это привело меня к моей первой кроличьей норе при проверке web.config. Содержимое файла не изменилось по сравнению с последним развертыванием. Разрешения NTFS для файла также не изменились. Насколько я понимаю, IIS часто выдает эту ошибку, когда проблема совершенно не связана с web.config, поэтому я начал действовать, предполагая, что это отвлекающий маневр.
Изменения кода
Я попробовал несколько вещей с базой кода и создал несколько последующих развертываний. Я попробовал все предложенные, которые смог найти:
Явный вызов builder.WebHost.UseStaticWebAssets();
Отключение CSS с ограниченной областью действия
Отключение PublishWithAspNetCoreTargetManifest
Удаление атрибута AuthorizeAttribute из _Imports.razor.
Использование AOT-компиляции.
Ничто из этого не дало никакого эффекта. Единственный способ предотвратить 500 ошибок, который я нашел, — это полностью отменить фиксацию, которая привела к такому поведению.
Локальное развертывание IIS
Я попробовал разместить приложение через IIS локально. Я скомпилировал и опубликовал код с теми же флагами, которые использую в конвейере CI для развертывания, добавил конфигурацию, идентичную рабочей, в appsettings.json (я знаю, что это небезопасно, не волнуйтесь, я использую переменные среды в prod) и указал IIS на папку.
При нажатии на localhost в браузере в средстве просмотра событий появилось 3 ошибки (Windows Журналы/приложение), все из PID IIS (модуль IIS AspNetCore V2). В хронологическом порядке это были:
Provided application path does not exist, or isn't a .dll or .exe.
Could not find 'aspnetcorev2_inprocess.dll'. Exception message: (сообщения об исключении не было)
Failed to start application '/LM/W3SVC/1/ROOT', ErrorCode '0x8000ffff'.
Я согласен, что в опубликованных двоичных файлах нет такого файла, как aspnetcorev2_inprocess.dll, ни в каком виде сборки, как до, так и после плохой фиксации, хотя у меня есть несколько копий в каталогах C:\Program Files, C:\Program Files (x86), %USERPROFILE%\.nuget\packages и %LOCALAPPDATA%.
Заключительные вопросы
Итак, мои вопросы:
Почему IIS завершается сбоем после введения этого коммита?
Как я могу изменить код и/или параметры компилятора так что у меня не возникает этих ошибок?
Полный анализ
Полный дамп коммита, вызвавшего эту проблему.
commit a24aa59fec75eb9e4f4d2d4fa3c7bc3309494144
Author: Josh Brunton
Date: Tue Oct 14 12:39:36 2025 +0100
feat: Add claims for auth
Make using the app dependent on being an activated worker in AX.
diff --git a/src/Doings.Web/Middleware/DoingsAuthzMiddleware.cs b/src/Doings.Web/Middleware/DoingsAuthzMiddleware.cs
new file mode 100644
index 0000000..3a6fa41
--- /dev/null
+++ b/src/Doings.Web/Middleware/DoingsAuthzMiddleware.cs
@@ -0,0 +1,42 @@
+using System.Security.Claims;
+using Doings.Lib.Ax.Common.Enums;
+using Doings.Lib.Ax.Common.Services;
+
+namespace Doings.Web.Middleware;
+
+public class DoingsAuthzMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly IServiceScopeFactory _scopeFactory;
+
+ public DoingsAuthzMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory)
+ {
+ _next = next;
+ _scopeFactory = scopeFactory;
+ }
+
+ public async Task InvokeAsync(HttpContext httpContext)
+ {
+ if (!httpContext.User.Identity!.IsAuthenticated)
+ {
+ await _next(httpContext);
+ return;
+ }
+
+ using var scope = _scopeFactory.CreateScope();
+ var doingsSharedDataService = scope.ServiceProvider.GetRequiredService();
+
+ var worker = await doingsSharedDataService.WhoAmI();
+ if (worker.IsActive is not NoYes.Yes)
+ {
+ await _next(httpContext);
+ return;
+ }
+
+ IEnumerable claims = [new("DoingsAuthz", "DoingsAuthz")];
+ ClaimsIdentity appIdentity = new(claims);
+ httpContext.User.AddIdentity(appIdentity);
+
+ await _next(httpContext);
+ }
+}
diff --git a/src/Doings.Web/Program.cs b/src/Doings.Web/Program.cs
index 9960291..3186274 100644
--- a/src/Doings.Web/Program.cs
+++ b/src/Doings.Web/Program.cs
@@ -1,9 +1,11 @@
using CommunityToolkit.Mvvm.Messaging;
using Doings.Application.Services;
using Doings.Web.Auth;
+using Doings.Web.Middleware;
using Doings.Web.View;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.Identity.Web;
@@ -45,22 +47,28 @@ internal class Program
builder.Services.AddScoped();
+ builder.Services.AddAuthorization(opts => opts.AddPolicy("DoingsAuthz", apBuilder =>
+ {
+ apBuilder.RequireClaim("DoingsAuthz");
+ }));
+
// Add services to the container.
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
bool doMock = !string.IsNullOrWhiteSpace(builder.Configuration["Doings:DoMock"]);
foreach (var s in ApplicationServiceHelper.GetInternallyImplementedServices(doMock))
{
builder.Services.Add(s);
}
return builder;
}
private static WebApplication CreateApp(WebApplicationBuilder builder)
{
var app = builder.Build();
+ app.UseMiddleware();
app.UseAuthorization();
app.UseExceptionHandler(new ExceptionHandlerOptions
{
diff --git a/src/Doings.Web/View/Components/RedirectComponent.cs b/src/Doings.Web/View/Components/RedirectComponent.cs
new file mode 100644
index 0000000..24d6ad4
--- /dev/null
+++ b/src/Doings.Web/View/Components/RedirectComponent.cs
@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Components;
+
+namespace Doings.Web.View.Components;
+
+public class RedirectComponent : ComponentBase
+{
+ private readonly NavigationManager _navMgr;
+
+ [Parameter] public string Uri { get; set; } = string.Empty;
+
+ public RedirectComponent(NavigationManager navMgr)
+ {
+ _navMgr = navMgr;
+ }
+
+ protected override void OnInitialized()
+ {
+ _navMgr.NavigateTo(Uri);
+ base.OnInitialized();
+ }
+}
diff --git a/src/Doings.Web/View/Layout/MinimalLayout.razor b/src/Doings.Web/View/Layout/MinimalLayout.razor
new file mode 100644
index 0000000..f6816d7
--- /dev/null
+++ b/src/Doings.Web/View/Layout/MinimalLayout.razor
@@ -0,0 +1,30 @@
+@inherits LayoutComponentBase
+
+
+
+
+
+
+
+
+ Doings
+
+
+
+
+ log out and try again with your own credentials.
+
+
+ If you're trying to access the service normally, please ensure that the above email has
+ a relevant Worker in D365, and that the Worker's IsActive status is set to Yes.
+ If you're trying to access a specific resource within the service, check that you have
+ the required claims.
+
+ @if (!string.IsNullOrWhiteSpace(ReturnUrl))
+ {
+
+ The page that sent you here recommends you return to
+ @NavMgr.ToAbsoluteUri(ReturnUrl).
+
+ }
+
+
+@code
+{
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public string ReturnUrl { get; set; } = string.Empty;
+
+ private string _userEmail = string.Empty;
+ protected override async Task OnInitializedAsync()
+ {
+ _userEmail = await User.GetLoggedInUserEmail();
+ await base.OnInitializedAsync();
+ }
+}
diff --git a/src/Doings.Web/View/Routes.razor b/src/Doings.Web/View/Routes.razor
index f756e19..8c0b06d 100644
--- a/src/Doings.Web/View/Routes.razor
+++ b/src/Doings.Web/View/Routes.razor
@@ -1,6 +1,19 @@
-
-
-
-
-
-
+@using Doings.Web.View.Layout
+@using Microsoft.AspNetCore.Components.Authorization
+@using Doings.Web.View.Components
+
+
+
+
+
+
+
+
+
+
+
Authorizing...
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Doings.Web/View/_Imports.razor b/src/Doings.Web/View/_Imports.razor
index 22100d6..4824c23 100644
--- a/src/Doings.Web/View/_Imports.razor
+++ b/src/Doings.Web/View/_Imports.razor
@@ -7,4 +7,7 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Doings.Web
-@using Microsoft.FluentUI.AspNetCore.Components
\ No newline at end of file
+@using Microsoft.FluentUI.AspNetCore.Components
+
+@using Microsoft.AspNetCore.Authorization
+@attribute [Authorize(Policy = "DoingsAuthz")]
\ No newline at end of file
У меня есть серверное приложение Blazor, которое требует от пользователей входа в систему с учетной записью Microsoft (в клиенте моей компании). Я пытаюсь обеспечить его дополнительную безопасность, разрешая только пользователям, которые соответствуют определенным критериям, но моя попытка сделать это делает приложение полностью неработоспособным в рабочей среде.[b]Причина проблемы Способ, которым я реализовал авторизацию (полная разница в конце вопроса), был следующим: [list] [*]Создал класс DoingsAuthzMiddleware, который добавит удостоверение, включая утверждение DoingsAuthz, проверенным пользователям. [*]Добавлен builder.Services.AddAuthorization(opts => opts.AddPolicy("DoingsAuthz", apBuilder => apBuilder.RequireClaim("DoingsAuthz"))); и app.UseMiddleware(); в мой Program.cs. [*]Добавлен @attribute [Authorize(Policy = "DoingsAuthz")] в _Imports.razor для защиты всех страниц по умолчанию. [*]Добавлен @attribute [AllowAnonymous] на страницы, которые должны разрешать анонимный доступ (например, страницу 403). [/list] Мне также пришлось внести некоторые изменения в мой Routes.razor, чтобы все состояние каскадной аутентификации работало, и коммит включает пару новых представлений для сценария 403. Симптомы Когда я запускаю локально с помощью dotnet run или любым другим способом, сайт работает так, как ожидалось. Оно допускает авторизованных пользователей, отклоняет неавторизованных и, по-видимому, не имеет других побочных эффектов. В рабочей среде приложение развертывается в веб-приложении Azure с использованием бесплатного номера SKU F1 и работает под управлением Windows со стеком среды выполнения dotnet v9.0. Когда я развернул этот и 18 других коммитов, при посещении главной страницы я столкнулся с ошибкой HTTP 500. Расследование Вперед и назад Сразу после неудачного развертывания я повторно развернул последний известный стабильный коммит. Это сработало, как и ожидалось. Затем я снова попытался развернуть плохую версию, чтобы исключить ошибку в этом конкретном развертывании. Я испытал те же симптомы, что и раньше. Затем я выполнил биссектрису, определив, что описанный выше коммит является нарушающим. web.config Используя az webapp logtail и нажав URL-адрес корневой страницы, я получил что-то похожее на HTML-страницу. Убрав весь ненужный HTML, мы получили вот такой контент: HTTP Error 500.0 - Internal Server Error
The page cannot be displayed because an internal server error has occurred.
Most likely causes:
- IIS received the request; however, an internal error occurred during the processing of the request. The root cause of this error depends on which module handles the request and what was happening in the worker process when this error occurred.
- IIS was not able to access the web.config file for the Web site or application. This can occur if the NTFS permissions are set incorrectly.
- IIS was not able to process configuration for the Web site or application.
- The authenticated user does not have permission to use this DLL.
- The request is mapped to a managed handler but the .NET Extensibility Feature is not installed.
Things you can try:
- Ensure that the NTFS permissions for the web.config file are correct and allow access to the Web server's machine account.
- Check the event logs to see if any additional information was logged.
- Verify the permissions for the DLL.
- Install the .NET Extensibility feature if the request is mapped to a managed handler.
- Create a tracing rule to track failed requests for this HTTP status code. For more information about creating a tracing rule for failed requests, go to http://go.microsoft.com/fwlink/?LinkID=66439.
Это привело меня к моей первой кроличьей норе при проверке web.config. Содержимое файла не изменилось по сравнению с последним развертыванием. Разрешения NTFS для файла также не изменились. Насколько я понимаю, IIS часто выдает эту ошибку, когда проблема совершенно не связана с web.config, поэтому я начал действовать, предполагая, что это отвлекающий маневр. Изменения кода Я попробовал несколько вещей с базой кода и создал несколько последующих развертываний. Я попробовал все предложенные, которые смог найти: [list] [*]Явный вызов builder.WebHost.UseStaticWebAssets(); [*]Отключение CSS с ограниченной областью действия [*]Отключение PublishWithAspNetCoreTargetManifest [*]Удаление атрибута AuthorizeAttribute из _Imports.razor. [*]Использование AOT-компиляции. [/list] Ничто из этого не дало никакого эффекта. Единственный способ предотвратить 500 ошибок, который я нашел, — это полностью отменить фиксацию, которая привела к такому поведению. Локальное развертывание IIS Я попробовал разместить приложение через IIS локально. Я скомпилировал и опубликовал код с теми же флагами, которые использую в конвейере CI для развертывания, добавил конфигурацию, идентичную рабочей, в appsettings.json (я знаю, что это небезопасно, не волнуйтесь, я использую переменные среды в prod) и указал IIS на папку. При нажатии на localhost в браузере в средстве просмотра событий появилось 3 ошибки (Windows Журналы/приложение), все из PID IIS (модуль IIS AspNetCore V2). В хронологическом порядке это были: [list] [*]Provided application path does not exist, or isn't a .dll or .exe. [*]Could not find 'aspnetcorev2_inprocess.dll'. Exception message: (сообщения об исключении не было) [*]Failed to start application '/LM/W3SVC/1/ROOT', ErrorCode '0x8000ffff'. [/list] Я согласен, что в опубликованных двоичных файлах нет такого файла, как aspnetcorev2_inprocess.dll, ни в каком виде сборки, как до, так и после плохой фиксации, хотя у меня есть несколько копий в каталогах C:\Program Files, C:\Program Files (x86), %USERPROFILE%\.nuget\packages и %LOCALAPPDATA%. Заключительные вопросы Итак, мои вопросы: [list] [*]Почему IIS завершается сбоем после введения этого коммита? [*]Как я могу изменить код и/или параметры компилятора так что у меня не возникает этих ошибок? [/list] Полный анализ Полный дамп коммита, вызвавшего эту проблему. commit a24aa59fec75eb9e4f4d2d4fa3c7bc3309494144 Author: Josh Brunton Date: Tue Oct 14 12:39:36 2025 +0100
feat: Add claims for auth
Make using the app dependent on being an activated worker in AX.
diff --git a/src/Doings.Web/Middleware/DoingsAuthzMiddleware.cs b/src/Doings.Web/Middleware/DoingsAuthzMiddleware.cs new file mode 100644 index 0000000..3a6fa41 --- /dev/null +++ b/src/Doings.Web/Middleware/DoingsAuthzMiddleware.cs @@ -0,0 +1,42 @@ +using System.Security.Claims; +using Doings.Lib.Ax.Common.Enums; +using Doings.Lib.Ax.Common.Services; + +namespace Doings.Web.Middleware; + +public class DoingsAuthzMiddleware +{ + private readonly RequestDelegate _next; + private readonly IServiceScopeFactory _scopeFactory; + + public DoingsAuthzMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory) + { + _next = next; + _scopeFactory = scopeFactory; + } + + public async Task InvokeAsync(HttpContext httpContext) + { + if (!httpContext.User.Identity!.IsAuthenticated) + { + await _next(httpContext); + return; + } + + using var scope = _scopeFactory.CreateScope(); + var doingsSharedDataService = scope.ServiceProvider.GetRequiredService(); + + var worker = await doingsSharedDataService.WhoAmI(); + if (worker.IsActive is not NoYes.Yes) + { + await _next(httpContext); + return; + } + + IEnumerable claims = [new("DoingsAuthz", "DoingsAuthz")]; + ClaimsIdentity appIdentity = new(claims); + httpContext.User.AddIdentity(appIdentity); + + await _next(httpContext); + } +} diff --git a/src/Doings.Web/Program.cs b/src/Doings.Web/Program.cs index 9960291..3186274 100644 --- a/src/Doings.Web/Program.cs +++ b/src/Doings.Web/Program.cs @@ -1,9 +1,11 @@ using CommunityToolkit.Mvvm.Messaging; using Doings.Application.Services; using Doings.Web.Auth; +using Doings.Web.Middleware; using Doings.Web.View; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Web; using Microsoft.FluentUI.AspNetCore.Components; using Microsoft.Identity.Web; @@ -45,22 +47,28 @@ internal class Program
builder.Services.AddScoped();
+ builder.Services.AddAuthorization(opts => opts.AddPolicy("DoingsAuthz", apBuilder => + { + apBuilder.RequireClaim("DoingsAuthz"); + })); + // Add services to the container. builder.Services.AddRazorComponents().AddInteractiveServerComponents();
bool doMock = !string.IsNullOrWhiteSpace(builder.Configuration["Doings:DoMock"]); foreach (var s in ApplicationServiceHelper.GetInternallyImplementedServices(doMock)) { builder.Services.Add(s); }
return builder; }
private static WebApplication CreateApp(WebApplicationBuilder builder) { var app = builder.Build();
+ app.UseMiddleware(); app.UseAuthorization(); app.UseExceptionHandler(new ExceptionHandlerOptions { diff --git a/src/Doings.Web/View/Components/RedirectComponent.cs b/src/Doings.Web/View/Components/RedirectComponent.cs new file mode 100644 index 0000000..24d6ad4 --- /dev/null +++ b/src/Doings.Web/View/Components/RedirectComponent.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Components; + +namespace Doings.Web.View.Components; + +public class RedirectComponent : ComponentBase +{ + private readonly NavigationManager _navMgr; + + [Parameter] public string Uri { get; set; } = string.Empty; + + public RedirectComponent(NavigationManager navMgr) + { + _navMgr = navMgr; + } + + protected override void OnInitialized() + { + _navMgr.NavigateTo(Uri); + base.OnInitialized(); + } +} diff --git a/src/Doings.Web/View/Layout/MinimalLayout.razor b/src/Doings.Web/View/Layout/MinimalLayout.razor new file mode 100644 index 0000000..f6816d7 --- /dev/null +++ b/src/Doings.Web/View/Layout/MinimalLayout.razor @@ -0,0 +1,30 @@ +@inherits LayoutComponentBase + + + + + + + [img]/img/logo-orange.png[/img] + [url=/] + Doings + [/url] + + + + [url=/logout]log out[/url] and try again with your own credentials. + + + If you're trying to access the service normally, please ensure that the above email has + a relevant Worker in D365, and that the Worker's IsActive status is set to Yes. + If you're trying to access a specific resource within the service, check that you have + the required claims. + + @if (!string.IsNullOrWhiteSpace(ReturnUrl)) + { + + The page that sent you here recommends you return to + [url=@ReturnUrl]@NavMgr.ToAbsoluteUri(ReturnUrl)[/url]. + + } + + +@code +{ + [Parameter] + [SupplyParameterFromQuery] + public string ReturnUrl { get; set; } = string.Empty; + + private string _userEmail = string.Empty; + protected override async Task OnInitializedAsync() + { + _userEmail = await User.GetLoggedInUserEmail(); + await base.OnInitializedAsync(); + } +} diff --git a/src/Doings.Web/View/Routes.razor b/src/Doings.Web/View/Routes.razor index f756e19..8c0b06d 100644 --- a/src/Doings.Web/View/Routes.razor +++ b/src/Doings.Web/View/Routes.razor @@ -1,6 +1,19 @@ - - - - - - +@using Doings.Web.View.Layout +@using Microsoft.AspNetCore.Components.Authorization +@using Doings.Web.View.Components + + + + + + + + + + + Authorizing... + + + + + \ No newline at end of file diff --git a/src/Doings.Web/View/_Imports.razor b/src/Doings.Web/View/_Imports.razor index 22100d6..4824c23 100644 --- a/src/Doings.Web/View/_Imports.razor +++ b/src/Doings.Web/View/_Imports.razor @@ -7,4 +7,7 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using Doings.Web -@using Microsoft.FluentUI.AspNetCore.Components \ No newline at end of file +@using Microsoft.FluentUI.AspNetCore.Components + +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Policy = "DoingsAuthz")] \ No newline at end of file