跳至主要內容

為你的 .NET Core (Blazor Server) 應用程式新增驗證 (Authentication)

提示:
  • 以下示範基於 .NET Core 8.0。該 SDK 與 .NET 6.0 或更高版本相容。
  • .NET Core 範例專案可在 GitHub 儲存庫 中找到。

先決條件​

安裝​

將 NuGet 套件新增到你的專案中:

dotnet add package Logto.AspNetCore.Authentication

整合​

新增 Logto 驗證 (Authentication)​

打開 Startup.cs(或 Program.cs),並新增以下程式碼以註冊 Logto 驗證 (Authentication) 服務:

Program.cs
using Logto.AspNetCore.Authentication;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLogtoAuthentication(options =>
{
options.Endpoint = builder.Configuration["Logto:Endpoint"]!;
options.AppId = builder.Configuration["Logto:AppId"]!;
options.AppSecret = builder.Configuration["Logto:AppSecret"];
});

AddLogtoAuthentication 方法將執行以下操作:

  • 將預設驗證 (Authentication) 機制設為 LogtoDefaults.CookieScheme。
  • 將預設挑戰機制設為 LogtoDefaults.AuthenticationScheme。
  • 將預設登出機制設為 LogtoDefaults.AuthenticationScheme。
  • 將 cookie 和 OpenID Connect 驗證處理程序新增至驗證機制。

登入和登出流程​

在繼續之前,我們需要澄清 .NET Core 驗證中介軟體中的兩個容易混淆的術語:

  1. CallbackPath:使用者登入後,Logto 將使用者重定向回的 URI(在 Logto 中稱為「重定向 URI」)
  2. RedirectUri:在 Logto 驗證中介軟體中完成必要操作後將被重定向到的 URI。

登入流程可以如下圖所示:


類似地,.NET Core 也有 SignedOutCallbackPath 和 RedirectUri 用於登出流程。

為了清晰起見,我們將它們稱為:

我們使用的術語.NET Core 術語
Logto 重定向 URICallbackPath
Logto 登出後重定向 URISignedOutCallbackPath
應用程式重定向 URIRedirectUri

關於基於重導的登入​

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析。

配置重定向 URI​

備註:

在以下的程式碼片段中,我們假設你的應用程式運行在 http://localhost:3000/。

首先,讓我們配置 Logto 重定向 URI。將以下 URI 新增到 Logto 應用程式詳細資訊頁面的「Redirect URIs」列表中:

http://localhost:3000/Callback

要配置 Logto 登出後重定向 URI,請將以下 URI 新增到 Logto 應用程式詳細資訊頁面的「Post sign-out redirect URIs」列表中:

http://localhost:3000/SignedOutCallback

更改預設路徑​

Logto 重定向 URI 的預設路徑是 /Callback,而 Logto 登出後重定向 URI 的預設路徑是 /SignedOutCallback。

如果沒有特殊需求,你可以保持不變。如果想更改,可以為 LogtoOptions 設定 CallbackPath 和 SignedOutCallbackPath 屬性:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// 其他配置...
options.CallbackPath = "/Foo";
options.SignedOutCallbackPath = "/Bar";
});

記得在 Logto 應用程式詳細資訊頁面中相應更新值。

新增路由​

由於 Blazor Server 使用 SignalR 在伺服器和客戶端之間進行通訊,這意味著直接操作 HTTP context 的方法(如發起挑戰或重定向)在從 Blazor 元件調用時無法如預期運作。

為了正確處理,我們需要明確地為登入和登出重定向新增兩個端點:

Program.cs
app.MapGet("/SignIn", async context =>
{
if (!(context.User?.Identity?.IsAuthenticated ?? false))
{
await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});

app.MapGet("/SignOut", async context =>
{
if (context.User?.Identity?.IsAuthenticated ?? false)
{
await context.SignOutAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});

現在,我們可以重定向到這些端點以觸發登入和登出。

實作登入和登出按鈕​

在 Razor 元件中,新增以下程式碼:

Components/Pages/Index.razor
@using Microsoft.AspNetCore.Components.Authentication
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager

@* ... *@

<p>是否已驗證 (Is authenticated): @User.Identity?.IsAuthenticated</p>
@if (User.Identity?.IsAuthenticated == true)
{
<button @onclick="SignOut">登出</button>
}
else
{
<button @onclick="SignIn">登入</button>
}

@* ... *@

@code {
private ClaimsPrincipal? User { get; set; }

protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
}

private void SignIn()
{
NavigationManager.NavigateTo("/SignIn", forceLoad: true);
}

private void SignOut()
{
NavigationManager.NavigateTo("/SignOut", forceLoad: true);
}
}

說明:

  • 注入的 AuthenticationStateProvider 用於獲取當前使用者的驗證狀態,並填充 User 屬性。
  • SignIn 和 SignOut 方法用於分別將使用者重定向到登入和登出端點。由於 Blazor Server 的特性,我們需要使用 NavigationManager 並啟用強制載入來觸發重定向。

如果使用者未驗證,頁面將顯示「登入」按鈕;如果使用者已驗證,則顯示「登出」按鈕。

<AuthorizeView /> 元件​

或者,你可以使用 AuthorizeView 元件根據使用者的驗證 (Authentication) 狀態有條件地渲染內容。當你想要對已驗證和未驗證的使用者顯示不同內容時,這個元件非常有用。

在你的 Razor 元件中,新增以下程式碼:

Components/Pages/Index.razor
@using Microsoft.AspNetCore.Components.Authorization

@* ... *@

<AuthorizeView>
<Authorized>
<p>名稱:@User?.Identity?.Name</p>
@* 已驗證使用者的內容 *@
</Authorized>
<NotAuthorized>
@* 未驗證使用者的內容 *@
</NotAuthorized>
</AuthorizeView>

@* ... *@

AuthorizeView 元件需要一個類型為 Task<AuthenticationState> 的級聯參數。獲取此參數的直接方法是新增 <CascadingAuthenticationState> 元件。然而,由於 Blazor Server 的特性,我們不能簡單地將元件新增到佈局或根元件(可能無法如預期運作)。相反地,我們可以在建構器(Program.cs 或 Startup.cs)中新增以下程式碼來提供級聯參數:

Program.cs
builder.Services.AddCascadingAuthenticationState();

然後,你可以在每個需要的元件中使用 AuthorizeView 元件。

檢查點:測試你的應用程式​

現在,你可以測試你的應用程式:

  1. 執行你的應用程式,你會看到登入按鈕。
  2. 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
  3. 登入後,你將被重定向回應用程式並看到登出按鈕。
  4. 點擊登出按鈕以清除權杖存儲並登出。

獲取使用者資訊​

顯示使用者資訊​

要確認使用者是否已驗證 (Authentication),可以檢查 User.Identity?.IsAuthenticated 屬性。

要取得使用者的宣告 (Claims),可以使用 User.Claims 屬性:

Controllers/HomeController.cs
var claims = User.Claims;

// 取得使用者 ID
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;

請參閱 LogtoParameters.Claims 以了解宣告名稱及其意義。

請求額外的宣告 (Claims)​

你可能會發現從 User.Claims 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openid、profile 和 offline_access。

要請求額外的權限範圍 (Scopes),可以在 options 物件中配置 Scopes 屬性:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Scopes = new string[] {
LogtoParameters.Scopes.Email,
LogtoParameters.Scopes.Phone
}
});

然後你可以透過 User.Claims 存取額外的宣告:

Controllers/HomeController.cs
var claims = User.Claims;

// 取得使用者電子郵件
var email = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Email)?.Value;

需要網路請求的宣告 (Claims)​

為了避免使用者物件過於龐大,某些宣告需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data 宣告,它也不會包含在使用者物件中。要獲取這些宣告,可以在 options 物件中將 GetClaimsFromUserInfoEndpoint 設為 true:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.GetClaimsFromUserInfoEndpoint = true;
});

權限範圍 (Scopes) 與宣告 (Claims)​

Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC 使用者資訊端點 (userinfo endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。

以下是支援的權限範圍 (Scopes) 及其對應的宣告 (Claims):

openid

宣告名稱類型描述需要使用者資訊嗎?
substring使用者的唯一識別符否

profile

宣告名稱類型描述需要使用者資訊嗎?
namestring使用者的全名否
usernamestring使用者的用戶名否
picturestring使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。否
created_atnumber使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。否
updated_atnumber使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。否

其他 標準宣告 包括 family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo 和 locale 也將包含在 profile 權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null。

備註:

與標準宣告不同,created_at 和 updated_at 宣告使用毫秒而非秒。

email

宣告名稱類型描述需要使用者資訊嗎?
emailstring使用者的電子郵件地址否
email_verifiedboolean電子郵件地址是否已驗證否

phone

宣告名稱類型描述需要使用者資訊嗎?
phone_numberstring使用者的電話號碼否
phone_number_verifiedboolean電話號碼是否已驗證否

address

請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。

custom_data

宣告名稱類型描述需要使用者資訊嗎?
custom_dataobject使用者的自訂資料是

identities

宣告名稱類型描述需要使用者資訊嗎?
identitiesobject使用者的連結身分是
sso_identitiesarray使用者的連結 SSO 身分是

roles

宣告名稱類型描述需要使用者資訊嗎?
rolesstring[]使用者的角色否

urn:logto:scope:organizations

宣告名稱類型描述需要使用者資訊嗎?
organizationsstring[]使用者所屬的組織 ID否
organization_dataobject[]使用者所屬的組織資料是

urn:logto:scope:organization_roles

宣告名稱類型描述需要使用者資訊嗎?
organization_rolesstring[]使用者所屬的組織角色,格式為 <organization_id>:<role_name>否

考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。

API 資源​

我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。

在應用程式中配置 API 資源​

一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Resource = "https://<your-api-resource-indicator>";
});

每個 API 資源都有其自身的權限(權限範圍)。

例如,https://shopping.your-app.com/api 資源具有 shopping:read 和 shopping:write 權限,而 https://store.your-app.com/api 資源具有 store:read 和 store:write 權限。

要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:

Program.cs
builder.Services.AddLogtoAuthentication(options =>
{
// ...
options.Resource = "https://shopping.your-app.com/api";
options.Scopes = new string[] {
"openid",
"profile",
"offline_access",
"read",
"write"
};
});

你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。

備註:

請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email 權限範圍,你也可以請求 email 權限範圍。不可用的權限範圍將被安全地忽略。

成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。

取得權杖​

透過 HttpContext 取得權杖​

有時你可能需要取得存取權杖 (Access token) 或 ID 權杖 (ID token) 來進行 API 呼叫。你可以使用 GetTokenAsync 方法來取得這些權杖:

var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessToken);
var idToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.IdToken);

不需要擔心權杖過期,驗證 (Authentication) 中介軟體會在必要時自動重新整理權杖。

警告:

雖然驗證 (Authentication) 中介軟體會自動重新整理權杖,但由於底層 OpenID Connect 驗證處理器的限制,使用者物件中的宣告 (Claims) 不會被更新。 這個問題可以在我們自行實作驗證處理器後解決。

請注意,上述的存取權杖 (Access token) 是用於 OpenID Connect 的 userinfo 端點的不透明權杖 (Opaque token),並非 JWT。如果你已指定 API 資源 (API resource),則需要使用 LogtoParameters.Tokens.AccessTokenForResource 來取得該 API 資源的存取權杖 (Access token):

var accessToken = await HttpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);

這個權杖將會是以 API 資源 (API resource) 為受眾 (Audience) 的 JWT。

在 Razor 元件中取得權杖​

由於我們無法在 Razor 元件中直接存取 HttpContext,因此需要將 HttpContextAccessor 注入到元件中,並使用它來獲取權杖。以下程式碼展示了如何在 Razor 元件中獲取 API 資源的存取權杖:

Components/Pages/Index.razor
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@using Logto.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IHttpContextAccessor HttpContextAccessor

@* ... *@

<p><b>資源 (Resource):</b> @(Resource ?? "(null)")</p>
<p><b>存取權杖 (Access Token):</b> @(AccessToken ?? "(null)")</p>

@* ... *@

@code {
private ClaimsPrincipal? User { get; set; }
private string? AccessToken { get; set; }
private string? Resource { get; set; }

protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;

if (User?.Identity?.IsAuthenticated == true)
{
await FetchTokenAsync();
}
}

private async Task FetchTokenAsync()
{
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext == null)
{
return;
}

var logtoOptions = httpContext.GetLogtoOptions();
Resource = logtoOptions?.Resource;
// 如有需要,替換為其他權杖類型
AccessToken = await httpContext.GetTokenAsync(LogtoParameters.Tokens.AccessTokenForResource);
}
}

延伸閱讀​

終端使用者流程:驗證流程、帳號流程與組織流程 (End-user flows: authentication flows, account flows, and organization flows) 設定連接器 (Configure connectors) 授權 (Authorization)