2026年4月29日 星期三

Blazor 10新功能介紹 - 3

 


在 .NET 10 的 Blazor中,安全性與前端互動能力都有重要提升。其中Passkey(通行密碼)支援讓 ASP.NET Core Identity 能直接實作無密碼登入,使用者可透過指紋、臉部辨識或硬體安全金鑰進行驗證,大幅提升安全性並降低密碼管理負擔。此外,Blazor 的 JavaScript Interop也導入更完整的物件語意,開發者可以直接呼叫 JavaScript 建構函式建立物件,並從 C# 讀寫 JavaScript 物件屬性。這些改進讓 Blazor 在身分驗證與前端整合方面更加現代化,也使開發流程更簡潔與安全。

本文將延續Blazor 10新功能介紹 - 1》、Blazor 10新功能介紹 - 2》一文的情境,介紹這些Blazor新功能。


Passkey(通行密碼)支援

Passkey(通行密碼)主要是取代或增強傳統密碼的加密憑證,讓使用者能夠透過設備原生的安全機制(如指紋辨識、臉部解鎖或硬體安全金鑰)來進行身分驗證,實現無密碼的登入體驗。Passkey主要優勢是在於能防範釣魚攻擊(Phishing-resistant)、安全且易於使用,同時解決了使用者記憶與管理複雜密碼的負擔。

.NET 10版的 ASP.NET Core Identity套件已經內建了Passkey 的支援,開發者不再需手動撰寫複雜的驗證程式碼。當你建立一個Blazor Web App 的專案,只要將「Authentication type」設定為「Individual Accounts」預設就包含了 Passkey 的管理與登入介面,以及提供註冊 Passkey、列出已註冊金鑰、刪除金鑰、使用 Passkey 登入,建立無密碼的帳號等流程的完整程式碼,請參考下圖所示:

1:建立使用ASP.NET Core Identity套件的專案


專案中會包含「PasskeyInputModel.cs」、「PasskeyOperation.cs」等定義型別程式檔,請參考下圖所示:

2Passkey相關型別

 

當網站執行時,可以直接選取首頁「Register」選單項目註冊一個使用者帳號,請參考下圖所示:

3:註冊使用者帳號


下一個畫面會跳出一個錯誤訊息,按下「Apply Migrations」按鈕,使用EF移轉建立資料庫,請參考下圖所示:

4:使用EF移轉建立資料庫


下一步點選「Click here to confirm your account」確認電子郵件,請參考下圖所示:

5:確認電子郵件


電子郵件確認完成,請參考下圖所示:

6:電子郵件確認


最後使用剛註冊的帳號登入網站,請參考下圖所示:

7:登入網站


進入帳號管理畫面,便可新增想使用的Passkey,請參考下圖所示: 

8:新增想使用的Passkey

 

JavaScript互動性(JavaScript Interop

.NET 10版中的Blazor,同樣在JavaScript互動性(JavaScript Interop)方面做了很大的改進。過去的 JavaScript 互動性主要停留在「功能性」層面,只能呼叫函式,或傳遞基本型別,而 .NET 10版則引入了真正的物件語意(Object Semantics),讓開發體驗更加簡潔與現代化。

在過去的版本中,要在 Blazor 建立一個 JavaScript 物件,通常需要使用「eval」函式,或透過全域變數來處理,這不僅容易污染全域命名空間,也不太安全。

現在,.NET 10版可以支援直接呼叫 JavaScript 建構函式(Constructor Function), 並且提供了一個「InvokeConstructorAsync」方法,讓你可以直接使用「new」運算子以非同步方式建立JavaScript 物件,不再需要使用 eval」函式,讓程式更簡潔易懂。

舉例來說下列的「sayhi.js」檔案中匯出一個「HiHelper」類別,其中包含一個「SayHi」方法會顯示一個歡迎訊息:

// 匯出 `HiHelper` 類別供 Blazor 端建立物件。

export class HiHelper {

    // 定義 `SayHi` 方法並接收外部傳入的名稱。

    SayHi(name) {

        // 如果傳入值是字串就去除前後空白,否則使用空字串。

        const displayName = typeof name === "string" ? name.trim() : "";

        // 如果有有效名稱就使用它,否則改用預設值 `Blazor`

        const finalName = displayName.length > 0 ? displayName : "Blazor";

        // 顯示包含名稱的提示訊息。

        alert(`HI, ${finalName}`);

    }

}

 

Blazor 元件中加入以下程式碼,讓使用者輸入名稱,按下按鈕後,呼叫JavaScript顯示歡迎訊息:

@page "/"

@rendermode InteractiveServer

@using Microsoft.JSInterop

@inject IJSRuntime JSRuntime

@implements IAsyncDisposable

 

<PageTitle>Home</PageTitle>

 

<div class="mt-4">

    <label for="nameInput" class="form-label">Name</label>

    <input id="nameInput" class="form-control" @bind="_name" @bind:event="oninput" />

    <button class="btn btn-primary mt-2" @onclick="InvokeSayHiAsync">Say Hi</button>

</div>

 

@code {

    // 儲存預設名稱,初始值為 `Blazor`

    private string _name = "Blazor";

    // 儲存匯入後的 JavaScript 模組參考。

    private IJSObjectReference? _module;

    // 儲存透過建構函式建立的 `HiHelper` 物件參考。

    private IJSObjectReference? _hiHelper;

 

    private async Task InvokeSayHiAsync() {

        // 如果名稱為空白,則設定成預設值 `Blazor`

        if (string.IsNullOrWhiteSpace(_name)) {

            _name = "Blazor";

        }

 

        // 如果模組尚未載入,則先匯入 `sayhi.js`

        _module ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/sayhi.js");

        // 如果 `HiHelper` 尚未建立,則建立 JavaScript 物件。

        _hiHelper ??= await _module.InvokeConstructorAsync("HiHelper");

 

        // 呼叫 JavaScript `SayHi` 方法並傳入名稱。

        await _hiHelper!.InvokeVoidAsync("SayHi", _name);

    }

 

    // 元件釋放時一併釋放 JavaScript 資源。

    public async ValueTask DisposeAsync() {

        // 如果 `HiHelper` 已建立,則釋放該物件。

        if (_hiHelper is not null) {

            await _hiHelper.DisposeAsync();

        }

 

        // 如果模組已載入,則釋放模組參考。

        if (_module is not null) {

            await _module.DisposeAsync();

        }

    }

}

 

這個範例執行的結果,請參考下圖所示:

9:呼叫JavaScript範例


讀取與修改物件屬性 (Property Access)

以前存取物件屬性可能需要額外寫 JavaScript 輔助函式,現在你可以直接從 C# 讀取或設定 JavaScript 物件的屬性(包含資料屬性與存取子屬性),而不需要額外撰寫 JavaScript 輔助函式:

●  讀取屬性:使用「GetValueAsync<TValue>("propertyName")」方法可以直接以非同步方式讀取屬性值。

●  設定屬性:使用「SetValueAsync<TValue>("propertyName", value) 」方法可以非同步方式更新屬性值;如果目標物件上尚未定義該屬性,甚至會自動建立該屬性。

 

我們將上個「sayhi.js」例子的程式改寫如下,加入「name」屬性:

// 匯出 `HiHelper` 類別供 Blazor 匯入使用。

export class HiHelper {

    // 建構式會在建立物件時執行。

    constructor() {

        // 設定 `name` 屬性的預設值為 `Blazor`

        this.name = "Blazor";

    }

    // 顯示打招呼訊息。

    SayHi(name) {

        // 如果傳入的是字串就去除前後空白,否則使用空字串。

        const displayName = typeof name === "string" ? name.trim() : "";

        // 如果有有效輸入名稱就使用它,否則改用物件上的 `name` 屬性。

        const finalName = displayName.length > 0 ? displayName : this.name;

        // 跳出提示視窗顯示問候訊息。

        alert(`Hi, ${finalName}`);

    }

}

 

這時只要修改Blazor元件的程式碼如下,讀寫JavaScript 物件的 name 屬性以組合出歡迎訊息,執行元件會得到和上例一樣的結果:

@page "/"

@rendermode InteractiveServer

@using Microsoft.JSInterop

@inject IJSRuntime JSRuntime

@implements IAsyncDisposable

 

<PageTitle>Home</PageTitle>

 

<div class="mt-4">

    <label for="nameInput" class="form-label">Name</label>

    <input id="nameInput" class="form-control" @bind="_name" @bind:event="oninput" />

    <button class="btn btn-primary mt-2" @onclick="InvokeSayHiAsync">Say Hi</button>

</div>

 

@code {

    // 儲存文字方塊中的名稱。

    private string? _name;

    // 儲存匯入後的 JavaScript 模組參考。

    private IJSObjectReference? _module;

    // 儲存 JavaScript `HiHelper` 物件參考。

    private IJSObjectReference? _hiHelper;

 

    private async Task InvokeSayHiAsync() {

        // 如果模組尚未載入,則先匯入 `sayhi.js`

        _module ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/sayhi.js");

        // 如果 `HiHelper` 尚未建立,則透過建構式建立物件。

        _hiHelper ??= await _module.InvokeConstructorAsync("HiHelper");

 

        // 當文字方塊有值時,將名稱寫到 JavaScript 物件的 `name` 屬性。

        if (!string.IsNullOrWhiteSpace(_name)) {

            await _hiHelper!.SetValueAsync("name", _name.Trim());

        }

 

        // JavaScript 物件的 `name` 屬性讀取目前名稱。

        var currentName = await _hiHelper.GetValueAsync<string>("name");

 

        // 呼叫 JavaScript `SayHi` 方法顯示訊息。

        await _hiHelper!.InvokeVoidAsync("SayHi", currentName);

    }

 

    // 元件釋放時同步釋放 JavaScript 物件資源。

    public async ValueTask DisposeAsync() {

        // 如果 `HiHelper` 已建立,則釋放它。

        if (_hiHelper is not null) {

            await _hiHelper.DisposeAsync();

        }

 

        // 如果模組已載入,則釋放它。

        if (_module is not null) {

            await _module.DisposeAsync();

        }

    }

}

 

使用「IJSInProcessObjectReference」型別

IJSInProcessObjectReference」是一種物件參考型別,可以用同步或非同步方式呼叫JavaScript,但它只適合在 Blazor WebAssembly專案之中使用。

IJSInProcessObjectReference」針對WebAssembly 同步執行進行了效能最佳化,很適用於注重效能的WebAssembly程式碼。

若把上例的情境搬到WebAssembly專案,在專案中加入以下「sayhi.js」:

// 匯出 `HiHelper` 類別供 Blazor 匯入使用。

export class HiHelper {

    // 建構式會在建立物件時執行。

    constructor() {

        // 設定 `name` 屬性的預設值為 `Blazor`

        this.name = "Blazor";

    }

 

    // 顯示打招呼訊息。

    SayHi(name) {

        // 如果傳入的是字串就去除前後空白,否則使用空字串。

        const displayName = typeof name === "string" ? name.trim() : "";

        // 如果有有效輸入名稱就使用它,否則改用物件上的 `name` 屬性。

        const finalName = displayName.length > 0 ? displayName : this.name;

        // 跳出提示視窗顯示問候訊息。

        alert(`Hi, ${finalName}`);

    }

}

 

export function createHiHelper() {

    // 建立並回傳一個新的 `HiHelper` 物件實例。

    return new HiHelper();

}


在首頁「Home.razor」加入以下程式碼,內嵌「HomeInteractive」元件,設定「rendermode」為「InteractiveWebAssembly」:

@page "/"

 

<PageTitle>Home</PageTitle>

 

<BlazorAppWASM.Client.Pages.HomeInteractive @rendermode=" " />

 

Client專案中加入「HomeInteractive.razor」,並在其中加入叫用JavaScript的程式碼,這樣執行程式就會得到和上例一樣的結果:

@implements IDisposable

@inject IJSRuntime JS

 

<div class="mt-4">

    <label for="nameInput" class="form-label">Name</label>

    <input id="nameInput" class="form-control" @bind="name" />

    <button class="btn btn-primary mt-2" @onclick="SayHi">Say Hi</button>

</div>

 

@code {

    // 儲存使用者在文字方塊中輸入的名稱。

    private string? name;

    // 儲存已匯入的 JS 模組參考,並支援同步呼叫。

    private IJSInProcessObjectReference? module;

    // 儲存 `HiHelper` JavaScript 物件參考,並支援同步呼叫。

    private IJSInProcessObjectReference? hiHelper;

 

    private void SayHi()

    {

        // 如果 `hiHelper` 已建立,同步執行 JS `SayHi` 方法,就將目前名稱傳到方法顯示問候訊息。

        hiHelper?.InvokeVoid("SayHi", name);

    }

 

    protected override async Task OnAfterRenderAsync(bool firstRender)

    {

        // 如果不是首次渲染,直接結束,避免重複初始化。

        if (!firstRender)

        {

            return;

        }

 

        // 非同步匯入 `/js/sayhi.js` 模組。

        var jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/sayhi.js");

 

        // 確認匯入後的模組是否支援同步呼叫介面。

        if (jsModule is not IJSInProcessObjectReference inProcessModule)

        {

            // 若不支援同步呼叫,拋出例外說明目前範例需求。

            throw new InvalidOperationException("This component requires IJSInProcessObjectReference.");

        }

 

        // 將可同步呼叫的模組參考存入欄位供後續重複使用。

        module = inProcessModule;

        // 透過模組中的工廠函式建立 `HiHelper` 物件參考。

        hiHelper = module.Invoke<IJSInProcessObjectReference>("createHiHelper");

    }

 

    // 在元件釋放時一併釋放已建立的 JS 物件參考。

    public void Dispose()

    {

        // 釋放 `HiHelper` 物件參考。

        hiHelper?.Dispose();

        // 釋放 JS 模組參考。

        module?.Dispose();

    }

}

 


總結

整體而言,.NET 10 Blazor 在安全驗證與 JavaScript 整合能力上更進一步。內建的 Passkey支援 ASP.NET Core Identity 能輕鬆實作無密碼登入機制,不僅提升使用者體驗,也能有效降低釣魚攻擊風險。同時,改進後的 JavaScript Interop 支援直接建立 JavaScript 物件、讀寫物件屬性,並在 WebAssembly 環境中提供同步呼叫的最佳化機制。這些新功能讓 Blazor 在建構現代 Web 應用時更具彈性與效率,也進一步強化了 .NET 在全端開發上的整體競爭力。

0 意見:

張貼留言