NET8 Blazor Web App 進階授權檢查與問題(BUG)

NET8, @attribute [Authorize], CustomAuthenticationStateProvider, AuthorizeView

引言

NET8 實作 Blazor Web App 過程發現一個授權檢查的bug。有進行加值授權檢驗的 @page【刷新頁面】時會出現 404

發生情境

<F5>或刷新指令【刷新頁面】都會出現 404 錯誤。 若由登入畫面開始“正常使用”不會出現這個問題。難怪偶有斷斷續續回報此問題但一直找不出原因。

若用 @attribute [Authorize] 基本檢驗 IsAuthenticated 則正常。

但若有在 CustomAuthenticationStateProvider 加值授權,這些加值的授權在【刷新頁面】時不會被加入, 導致不管是 Policy-base 或 Role-base 的加值授權檢查都會失敗! => 內部未知錯誤處過程 => 404。

@attribute [Authorize("AuthPage")] // Policy-base
@attribute [Authorize(Roles = "EXT-ROLE1,EXT-ROLE2")] //  Role-base

解法方案

AuthorizeView 執行加值授權檢查就正常了。因為 AuthorizeView 的授權資料一定會經過 AuthenticationStateProvider 加值。

解法一:直接使用 AuthorizeView

以 Policy-base 為例。

直接使用 AuthorizeView 包裝 page。缺點就是需再包裝一層。 如下:

DEMO013.razor
@page "/demo013"
@attribute [Page("DEMO013", "展示功能13")]

<AuthorizeView Policy="AuthPage" Resource=@this>
  @* ...page content... *@
</AuthorizeView>

@code {
  ...略...
}

解法二:客製 AuthorizePage 元件

客製 AuthorizePage 元件執行加值授權檢查。以 AuthorizeView 為基底實作。

DEMO013.razor
@page "/demo013"
@attribute [Page("DEMO013", "展示功能13")]

@* 取代 @attribute [Authorize("AuthPage")]。因為 F5 刷新會 => 404! *@
<AuthorizePage Page=@this />

@* ...page content... *@

@code {
  ...略...
}

AuthorizePage 客製元件

Components\Shared\AuthorizePage.razor
@using AsvtTPL.Components.Account.Shared
@inject NavigationManager navSvc

@*
* 取代 @attribute [Authorize("AuthPage")]。
* 因為 F5 刷新會 => 404!
* 注意:必需搭配 AOP.PageAttribute 宣示。例:
* ```
* @attribute [Page("DEMO013", "展示功能13")]
* ```
*@

<AuthorizeView Policy="AuthPage" Resource=@pageType>
  @* <Authorized>
  <h1 style="color:green;">允許使用(for debug)</h1>
  </Authorized> *@
  <Authorizing>
    @* 授權檢查中 *@
    <MudProgressLinear Color=Color.Error Indeterminate />
  </Authorizing>
  <NotAuthorized>
    @* 403 授權不足 禁止使用此功能 *@
    <RedirectToErrorStatus StatusCode=403 />
  </NotAuthorized>
</AuthorizeView>

@code {
  [CascadingParameter] Task<AuthenticationState> AuthStateTask { get; set; } = default!;

  /// <summary>
  /// 必需一開始就指定
  /// </summary>
  [Parameter, EditorRequired] public ComponentBase Page { get; set; } = default!;

  Type pageType = default!;

  protected override void OnInitialized()
  {
    pageType = Page.GetType();
  }

  protected override async Task OnParametersSetAsync()
  {
    //# RedirectToLogin when not IsAuthenticated.
    var authState = await AuthStateTask;
    if (!(authState.User.Identity?.IsAuthenticated ?? false))
      navSvc.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(navSvc.Uri)}", forceLoad: true);
      /// 或 => 401
  }
}

RedirectToErrorStatus 驗證失敗就轉址 403

Components\Shared\RedirectToErrorStatus.razor
@inject NavigationManager nvaSvc

@code {
  [Parameter] public int StatusCode { get; set; }

  protected override void OnInitialized()
  {
    nvaSvc.NavigateTo($"/ErrorStatus/{StatusCode}", forceLoad: false);
  }
}

相關 Policy-base 檢驗程式碼

Services\AuthPageRequirement.cs
using Microsoft.AspNetCore.Authorization;
using System.Reflection;

namespace YourProject.Services;

// 宣告 Authorization Requirement
// ※ 不必實作內容,只做為需求的宣示。
class AuthPageRequirement : IAuthorizationRequirement { }

// 實作 handler --- Authorization Requirement 檢驗程序
internal class AuthPageHandler(ILogger<AuthPageHandler> _logger) 
  : AuthorizationHandler<AuthPageRequirement>
{
  // 檢驗程序實作
  protected override Task HandleRequirementAsync(
      AuthorizationHandlerContext context, 
      AuthPageRequirement requirement)
  {
    //# 未登入離開。
    if (!context.User.Identity?.IsAuthenticated ?? false)
      return Task.CompletedTask;

    //# 取得需要資源,若非預期資源離開。
    Type? pageType = context.Resource as Type;
    if (pageType == null) return Task.CompletedTask;

    //# 取得客製資源
    // AuthPage 屬性 := @attribute[Page("DEMO013", "展示功能13")]
    PageAttribute? pageAttr = pageType.GetCustomAttribute<PageAttribute>();
    if (pageAttr == null) return Task.CompletedTask;
    // 功能代碼
    string funcCode = pageAttr.FuncId;

    //# 開始驗證
    // 是否登入者有授權的功能清單。
    if (!context.User.IsInRole(funcCode)) return Task.CompletedTask;

    // OK
    context.Succeed(requirement); // 滿足此項授權需求
    _logger.LogInformation($"User [{context.User.Identity!.Name}] has checked AuthPageRequirement successfully.");
    return Task.CompletedTask;
  }
}

別忘了在 Program.cs 註冊

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container. 
...略...

builder.Services.AddAuthentication(...);

// 註冊 客製 Policy 與滿足它所要的需求 Authorization Requirement
builder.Services.AddAuthorization(options =>
{
  options.AddPolicy("AuthPage", policy => policy.Requirements.Add(new AuthPageRequirement()));
});

// 註冊 客製化授權需求 Authorization Requirement。
// ※ 將自動觸發對應的檢驗程序 
builder.Services.AddSingleton<IAuthorizationHandler, AuthPageHandler>();

...略...
var app = builder.Build();

// Configure the HTTP request pipeline.
...略...

參考文章

(EOF)

Last updated