2020年2月23日 星期日

Blazor Server 的不正確使用者登入驗證做法 - 透過 Blazor 元件

Blazor Server 的不正確使用者登入驗證做法 - 透過 Blazor 元件

更多關於 Blazor 教學影片,可以參考 Blazor 教學影片播放清單 或者 Blazor 快速體驗教學影片撥放清單。也歡迎訂閱本 .NET / Blazor / Xamarin.Forms 影片頻道 。


當要進行 Blazor Server 專案開發的時候,若想要設計具有使用者身分驗證的功能,很理所當然的會想說,在 Blazor 專案內建立一個 Blazor 元件,在該元件上加入登入用的帳號與密碼與登入按鈕,想說這樣就可以了,不過,這可能會帶來問題,在這篇文章中將會來說明所會遇到的問題。在這裡的開發練習專案中,將會試圖想要設計一個使用 Cookie 的使用者身分驗證的功能。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

使用預設專案範本建立 Blazor 專案

想要進行這樣的專案開發練習,可以參考底下的操作步驟
  • 打開 Visual Studio 2019 開發工具
  • 當 [Visual Studio 2019] 對話窗出現之後,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,請找出 [Blazor 應用程式] 這個專案開發範本,並且點選這個專案開發範本
  • 請點選右下角 [下一步] 按鈕
  • 出現 [設定新的專案] 對話窗,輸入適當的 [專案名稱] 、 [位置] ,完成後,請點選右下角 [建立] 按鈕
    在這個範例程式碼中,將會建立一個 BlazorSignInAsync 專案名稱
  • 此時將會看到 [建立新的 Blazor 應用程式] 對話窗,這裡可以根據當時開發專案的需要,自行決定是否有調整 Blazor 專案的其他特性,若無,請點選右下角的 [建立] 按鈕
  • 此時,這個 Blazor 專案已經建立完成

修正 Startup.cs

  • 請打開 Startup.cs 這個檔案
  • 找到 ConfigureServices 方法
  • 在該方法的最後面加入底下程式碼
#region 加入使用 Cookie 認證需要的宣告
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(
    CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
#endregion
這裡將會註冊 IHttpContextAccessor 這個服務,因為,想要能夠在 Blazor 元件內來使用 HttpContext 這個物件
另外,也會啟用 Cookie 身分認證功能
  • 找到 Configure 方法
  • 在 app.UseStaticFiles(); 敘述下面,加入這段程式碼
app.UseAuthentication();

設計一個登入 Blazor 元件 Component

  • 滑鼠右擊這個專案節點內的 [Pages] 資料夾
  • 選擇 [加入] > [新增項目] 選項
  • 在 [新增項目] 對話窗內,選擇 [Razor 元件] 選項
  • 在下方 [名稱] 欄位內,輸入 BlazorLoginLogout.razor
  • 點選 [新增] 按鈕,完成這個登入 Razor 頁面的建立
  • 使用底下的 Razor 程式碼,替換掉產生出來的程式碼
@using Microsoft.AspNetCore.Identity;
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Authentication
@using System.Security.Claims
@using Microsoft.AspNetCore.Authentication.Cookies

@inject IHttpContextAccessor HttpContextAccessor
@inject NavigationManager NavigationManager

@page  "/BlazorAuth"

<h3>Blazor 的登入與登出身分驗證</h3>

<div class="row">
    <div class="col-md-12 bg-light">
        <EditForm Model="@userModel" OnValidSubmit="@LoginAsync">
            <div class="form-group">
                <label>帳號: </label>
                <input @bind-value="userModel.Email" class="form-control" />
            </div>
            <div class="form-group">
                <label>密碼: </label>
                <input type="password" @bind-value="userModel.Password" class="form-control" />
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-primary">登入</button>
            </div>
        </EditForm>
    </div>
</div>

@code {
    public UserModel userModel { get; set; } = new UserModel();

    public async Task LoginAsync()
    {

        var claims = new List<Claim>()
        {
            new Claim(ClaimTypes.Name, userModel.Email),
            new Claim("UserID", "123")
        };

        var claimsIdentity = new ClaimsIdentity(claims, "myTest");
        var principal = new ClaimsPrincipal(claimsIdentity);

        await HttpContextAccessor.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                        principal,
                        new AuthenticationProperties
                        {
                            ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
                            IsPersistent = false,
                            AllowRefresh = true
                        });
        NavigationManager.NavigateTo("/");
    }

    public class UserModel
    {
        public string Email { get; set; }
        public string Password { get; set; }
    }
}
首先,在該 Razor 元件的最前面,將會使用 @using 這樣的語法,加入這個 Blazor 元件內將會使用到各個 API 用到的命名空間,另外,也會使用 @inject 語法來使用相依注入 Dependency Injection 容器服務 Container Server (DI Container)來注入 IHttpContextAccessor & NavigationManager 這兩個服務物件到這個 Razor 元件內。
對於 HTML 標記內容而言,則是非常簡單的使用這個 Blazor 內建的 EditForm 元件,來設計出一個輸入帳號與密碼的頁面,當使用者點選了 [登入] 按鈕之後,便會觸發 EditForm 元件中的 OnValidSubmit 事件參數,也就是會執行 LoginAsync 這個方法。
在 LoginAsync 方法內,將不會檢查 帳號與密碼是否正確,因為,這裡只是要進行使用者的身分驗證執行程序,因此,將會建立起一個 ClainsPrincipal 這個 principal 物件,接者,透過相依注入的 HttpContextAccessor 物件,便可以取得 HttpContext 物件;因為已經加入了 Microsoft.AspNetCore.Authentication 命名空間參考,所以,便可以呼叫 SignInAsync 這個方法,來進行新的使用者登入作業。

修改 Index.razor Blazor 元件

現在,要在 Index.razor 元件中,來顯示這些靜態圖片
  • 在 Pages 資料夾內找到 Index.razor 檔案
  • 打開這個檔案,使用底下程式碼進行替換
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<BlazorLoginLogout/>

查看執行結果

好的,可以來執行這個專案
請在帳號與密碼欄位中隨意輸入任何文字,並且點選 登入 按鈕,將會看到如下面截圖的最下方的錯誤訊息。
想要知道發生了甚麼問題,可以先按下 [F12] ,啟動開發人員工具。
接著,按下 [F5] 按鈕,重新整理這個網頁,便可以重新輸入任意文字到帳號與密碼欄位上,而後再一次點選 登入按鈕,現在,便可以在開發人員工具視窗中,看到底下的錯誤訊息。
Error: System.InvalidOperationException: Headers are read-only, response has already started.

   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()

   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)

   at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value, CookieOptions options)

   at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie(HttpContext context, String key, String value, CookieOptions options)

   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)

   at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)

   at BlazorSignInAsync.Pages.BlazorLoginLogout.LoginAsync() in D:\Vulcan\Projects\BlazorSignInAsync\BlazorSignInAsync\Pages\BlazorLoginLogout.razor:line 47

   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)

   at Microsoft.AspNetCore.Components.Forms.EditForm.HandleSubmitAsync()

   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)

   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
從例外異常的訊息中 Error: System.InvalidOperationException: Headers are read-only, response has already started. 可以明確的看出來,現在這個樣的 Blazor 使用者登入身分驗證的方式(使用 Blazor 元件與 SignInAsync 方法) 所設計出來的登入做法,是不成功的。

沒有留言:

張貼留言