Blazor進階: Blazor Server and the Logout Problem

Blazor Server App, 多 TabPage, 多 Tab Page,

參考文章

重點資訊

完整文章請參考原文 Blazor Server and the Logout Problem。重新描述重點如下:

灰色登入狀態情境:開啟網頁登入後,再多次開啟成多個不同 TabPage 時,此時所有 TabPage 都是登入狀態,當我們在其中一個 TabPage 執行登出時其它 TabPage 卻仍一直處在登入狀態,直到 reload page 時才會發現早已登出。這是嚴重的問題。該『灰色登入狀態』就前端來說真的仍處在登入狀態,而後端已是登出了。若此時數據操作不當可能在明明已經登出了卻還能執行登入操作

原因:以 Blazor Server App 來說若以 Cookie Authentication 來登入驗證。該 Cookie 狀態只有在 reload page 時才會更新,平常都是處與 SPA/SSR 狀態。在多 TabPage 情境下其中一個登出時頂多強制 reload 本身網頁其它 TabPage 是無感的。

所以:原則上不要在前端元件隨意的使用 IHttpContextAccessor 來存取 HttpContext,要謹慎的使用因為 SignalR/WebSocket 與 HTTP 通訊協定原則上是不能直接互通的。不過在 reload page 時還是會重新刷新 cookie 這個 HTTP 協定的狀態。

解決方案

一、後端部份:在後端操作數據時,一定要再檢查使用者是否處在登入狀態。這只能自己處理。

二、前端部份:因為多 TagPage 下各個 TabPage 都是獨立的個體。現在(2023/10) Blazor Server App 的標準答案是每隔幾秒在後端檢查若使用已登入就強制登出當前的 TabPage。

關鍵程式碼

在前端部份,實作 CustomAuthenticationStateProvider 的繼承父親由 AuthenticationStateProvider 改成 RevalidatingServerAuthenticationStateProvider。  

其中 override property: RevalidationInterval --- 指定每間隔幾秒要去確認使用者是否已登出。

其中 override method: ValidateAuthenticationStateAsync --- 確認使用者是否已登出。

CustomAuthenticationStateProvider.cs
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Http;

/// <summary>
/// 加值預設的 AuthenticationStateProvider。
/// </summary>
internal class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
  //# inject 
  readonly IHttpContextAccessor _http; 
  //※ 不要在前端元件使用 IHttpContextAccesser 因為 HttpContext 的狀態並非即時反應的。
  readonly AccountService _accSvc;

  // resource
  readonly AuthenticationState anonymousUser;

  public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IHttpContextAccessor http, AccountService accSvc) : base(loggerFactory)
  {
    _http = http;
    _accSvc = accSvc;
    anonymousUser = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
  }

  /// <summary>
  /// 每隔幾秒查看是否還在登入狀態。
  /// 因為 HttpContext 的 Cookie 並非即時反應的。這會導致在多畫面應用情境時,若一畫面登出其他畫面卻仍在登入狀態的不同步冏狀。
  /// </summary>
  protected override TimeSpan RevalidationInterval => TimeSpan.FromSeconds(7);

  public override Task<AuthenticationState> GetAuthenticationStateAsync()
  {
    //※ --- 本例為 Cookie Authentication 仍會用到 HttpContext。 
    var userIdentity = _http.HttpContext?.User.Identity as ClaimsIdentity;
	
    //# 沒有 AuthCookie,回傳anonymousUser。
    if (userIdentity == null || !userIdentity.IsAuthenticated)
      return Task.FromResult(anonymousUser);

    //# 取授權資料
    var authUser = _accSvc.GetAuthDataFromPool(userIdentity.Name!);
    ...

    //## 加值授權資料
    ...

    //## 登入完成
    var userAuthState = new AuthenticationState(new ClaimsPrincipal(userIdentity));
    return Task.FromResult(userAuthState);
  }

  /// <summary>
  /// 查看是否還在登入狀態。與 RevalidationInterval 搭配。
  /// </summary>
  protected override Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authState, CancellationToken cancellationToken)
  {
    string userId = authState.User.Identity?.Name ?? string.Empty;
    bool isLogged = _accSvc.IsUserLoggedIn(userId); //--- 查看使用者登入狀態。
    return Task.FromResult(isLogged);
    /// ※ 確認使用者仍是登入,若不是後續將會強制登出。
  }
}

(EOF)

Last updated