《進擊吧!Blazor!》是本人與張善友老師合作的Blazor零基礎入門教程視訊,此教程能讓一個從未接觸過Blazor的程式設計師掌握開發Blazor應用的能力。
視訊地址:https://space.bilibili.com/483888821/channel/detail?cid=151273
Blazor WebAssembly 是單頁應用 (SPA) 框架,用於使用 .NET 生成互動式客戶端 Web 應用,採用 C# 代替 JavaScript 來編寫前端程式碼
本系列文章因篇幅有限,省略了部分程式碼,完整示例程式碼:https://github.com/TimChen44/Blazor-ToDo
作者:陳超超
Ant Design Blazor 專案貢獻者,擁有十多年從業經驗,長期基於.Net 技術棧進行架構與開發產品的工作,現就職於正泰集團。
郵箱:timchen@live.com
歡迎各位讀者有任何問題聯絡我,我們共同進步。
我的的 ToDo 應用基本功能已經完成,但是自己的待辦當然只有自己知道,所以我們這次給我們的應用增加一些安全方面的功能。
Blazor 身份驗證與授權
身份驗證
Blazor Server 應用和 Blazor WebAssembly 應用的安全方案有所不同。
- Blazor WebAssembly
Blazor WebAssembly 應用在客戶端上執行。 由於使用者可繞過客戶端檢查,因為使用者可修改所有客戶端程式碼, 因此授權僅用於確定要顯示的 UI 選項,所有客戶端應用程式技術都是如此。
- Blazor Server
Blazor Server 應用通過使用 SignalR 建立的實時連線執行。 建立連線後,將處理基於 SignalR 的應用的身份驗證。 可基於 cookie 或一些其他持有者令牌進行身份驗證。
授權
AuthorizeView
元件根據使用者是否獲得授權來選擇性地顯示 UI 內容。 如果只需要為使用者顯示資料,而不需要在過程邏輯中使用使用者的標識,那麼此方法很有用。
<AuthorizeView>
<Authorized>
<!--驗證通過顯示-->
</Authorized>
<NotAuthorized>
<!--驗證不通過顯示-->
</NotAuthorized>
</AuthorizeView>
Blazor 中使用 Token
在 Blazor WebAssembly 模式下, 因為應用都在客戶端執行,所以使用 Token 作為身份認證的方式是一個比較好的選擇。
基本的使用時序圖如下
對於安全要求不高的應用採用這個方法簡單、易維護,完全沒有問題。
但是 Token 本身在安全性上存在以下兩個風險:
- Token 無法登出,所以可以在 Token 有效期內傳送的非法請求,服務端無能為力。
- Token 通過 AES 加密儲存在客戶端,理論上可以進行離線破解,破解後就能任意偽造 Token。
因此遇到安全要求非常高的應用時,我們需要認證服務進行 Token 的有效性驗證
改造 ToDo
接著我們對之前的 ToDo 專案進行改造,讓他支援登入功能。
ToDo.Shared
先把前後端互動所需的 Dto 建立了
public class LoginDto
{
public string UserName { get; set; }
public string Password { get; set; }
}
public class UserDto
{
public string Name { get; set; }
public string Token { get; set; }
}
ToDo.Server
先改造服務端,新增必要引用,編寫身份認證程式碼等
新增引用
- Microsoft.AspNetCore.Authentication.JwtBearer
Startup.cs
新增 JwtBearer 配置
public void ConfigureServices(IServiceCollection services)
{
//......
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,//是否驗證Issuer
ValidateAudience = true,//是否驗證Audience
ValidateLifetime = true,//是否驗證失效時間
ValidateIssuerSigningKey = true,//是否驗證SecurityKey
ValidAudience = "guetClient",//Audience
ValidIssuer = "guetServer",//Issuer,這兩項和簽發jwt的設定一致
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("123456789012345678901234567890123456789"))//拿到SecurityKey
};
});
}
此處定義了 Token 的金鑰,規則等,實際專案時可以將這些資訊放到配置中。
AuthController.cs
行政驗證控制器,用於驗證使用者身份,建立 Token 等。
[ApiController]
[Route("api/[controller]/[action]")]
public class AuthController : ControllerBase
{
//登入
[HttpPost]
public UserDto Login(LoginDto dto)
{
//模擬獲得Token
var jwtToken = GetToken(dto.UserName);
return new() { Name = dto.UserName, Token = jwtToken };
}
//獲得使用者,當頁面客戶端頁面重新整理時呼叫以獲得使用者資訊
[HttpGet]
public UserDto GetUser()
{
if (User.Identity.IsAuthenticated)//如果Token有效
{
var name = User.Claims.First(x => x.Type == ClaimTypes.Name).Value;//從Token中拿出使用者ID
//模擬獲得Token
var jwtToken = GetToken(name);
return new UserDto() { Name = name, Token = jwtToken };
}
else
{
return new UserDto() { Name = null, Token = null };
}
}
public string GetToken(string name)
{
//此處加入賬號密碼驗證程式碼
var claims = new Claim[]
{
new Claim(ClaimTypes.Name,name),
new Claim(ClaimTypes.Role,"Admin"),
};
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("123456789012345678901234567890123456789"));
var expires = DateTime.Now.AddDays(30);
var token = new JwtSecurityToken(
issuer: "guetServer",
audience: "guetClient",
claims: claims,
notBefore: DateTime.Now,
expires: expires,
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
ToDo.Client
改造客戶端,讓客戶端支援身份認證
新增引用
- Microsoft.AspNetCore.Components.Authorization
AuthenticationStateProvider
AuthenticationStateProvider
是 AuthorizeView
元件和 CascadingAuthenticationState
元件用於獲取身份驗證狀態的基礎服務。
通常不直接使用 AuthenticationStateProvider
,直接使用主要缺點是,如果基礎身份驗證狀態資料發生更改,不會自動通知元件。其次是專案中總會有一些自定義的認證邏輯。
所以我們通常寫一個類繼承他,並重寫一些我們自己的邏輯。
//AuthProvider.cs
public class AuthProvider : AuthenticationStateProvider
{
private readonly HttpClient HttpClient;
public string UserName { get; set; }
public AuthProvider(HttpClient httpClient)
{
HttpClient = httpClient;
}
public async override Task<AuthenticationState> GetAuthenticationStateAsync()
{
//這裡獲得使用者登入狀態
var result = await HttpClient.GetFromJsonAsync<UserDto>($"api/Auth/GetUser");
if (result?.Name == null)
{
MarkUserAsLoggedOut();
return new AuthenticationState(new ClaimsPrincipal());
}
else
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, result.Name));
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "apiauth"));
return new AuthenticationState(authenticatedUser);
}
}
/// <summary>
/// 標記授權
/// </summary>
/// <param name="loginModel"></param>
/// <returns></returns>
public void MarkUserAsAuthenticated(UserDto userDto)
{
HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", userDto.Token);
UserName = userDto.Name;
//此處應該根據伺服器的返回的內容進行配置本地策略,作為演示,預設新增了“Admin”
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, userDto.Name));
claims.Add(new Claim("Admin", "Admin"));
var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "apiauth"));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
//慈湖可以可以將Token儲存在本地儲存中,實現頁面重新整理無需登入
}
/// <summary>
/// 標記登出
/// </summary>
public void MarkUserAsLoggedOut()
{
HttpClient.DefaultRequestHeaders.Authorization = null;
UserName = null;
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
}
NotifyAuthenticationStateChanged
方法會通知身份驗證狀態資料(例如 AuthorizeView)使用者使用新資料重新呈現。
HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", userDto.Token);
將 HTTP 請求頭中加入 Token,這樣之後所有的請求都會帶上 Token。
在Program
中注入AuthProvider
服務,以便於其他地方使用
//Program.cs
builder.Services.AddScoped<AuthenticationStateProvider, AuthProvider>();
在Program
中配置支援的策略
builder.Services.AddAuthorizationCore(option =>
{
option.AddPolicy("Admin", policy => policy.RequireClaim("Admin"));
});
登入介面
新增Login.razor
元件,程式碼如下
<div style="margin:100px">
<Spin Spinning="isLoading">
@if (model != null) {
<form
OnFinish="OnSave"
Model="@model"
LabelCol="new ColLayoutParam() {Span = 6 }"
>
<FormItem Label="使用者名稱">
<input @bind-Value="context.UserName" />
</FormItem>
<FormItem Label="密碼">
<input @bind-Value="context.Password" type="password" />
</FormItem>
<FormItem WrapperColOffset="6">
<button type="@ButtonType.Primary" HtmlType="submit">登入</button>
</FormItem>
</form>
}
</Spin>
</div>
public partial class Login
{
[Inject] public HttpClient Http { get; set; }
[Inject] public MessageService MsgSvr { get; set; }
[Inject] public AuthenticationStateProvider AuthProvider { get; set; }
LoginDto model = new LoginDto();
bool isLoading;
async void OnLogin()
{
isLoading = true;
var httpResponse = await Http.PostAsJsonAsync<LoginDto>($"api/Auth/Login", model);
UserDto result = await httpResponse.Content.ReadFromJsonAsync<UserDto>();
if (string.IsNullOrWhiteSpace(result?.Token) == false )
{
MsgSvr.Success($"登入成功");
((AuthProvider)AuthProvider).MarkUserAsAuthenticated(result);
}
else
{
MsgSvr.Error($"使用者名稱或密碼錯誤");
}
isLoading = false;
InvokeAsync( StateHasChanged);
}
}
登入介面程式碼很簡單,就是向api/Auth/Login
請求,根據返回的結果判斷是否登入成功。
((AuthProvider)AuthProvider).MarkUserAsAuthenticated(result);
標記身份認證狀態已經修改。
修改佈局
修改MainLayout.razor
檔案
<CascadingAuthenticationState>
<AuthorizeView>
<Authorized>
<Layout>
<Sider Style="overflow: auto;height: 100vh;position: fixed;left: 0;">
<div class="logo">進擊吧!Blazor!</div>
<menu Theme="MenuTheme.Dark" Mode="@MenuMode.Inline">
<menuitem RouterLink="/"> 主頁 </menuitem>
<menuitem RouterLink="/today" RouterMatch="NavLinkMatch.Prefix">
我的一天
</menuitem>
<menuitem RouterLink="/star" RouterMatch="NavLinkMatch.Prefix">
重要任務
</menuitem>
<menuitem RouterLink="/search" RouterMatch="NavLinkMatch.Prefix">
全部
</menuitem>
</menu>
</Sider>
<Layout Class="site-layout"> @Body </Layout>
</Layout>
</Authorized>
<NotAuthorized>
<ToDo.Client.Pages.Login></ToDo.Client.Pages.Login>
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
當授權通過後顯示<AuthorizeView>
中<Authorized>
的選單及主頁,反之顯示<NotAuthorized>
的Login
元件內容。
當需要根據許可權顯示不同內容,可以使用<AuthorizeView>
的Policy
屬性實現,具體是在AuthenticationStateProvider
中通過配置策略,比如示例中claims.Add(new Claim("Admin", "Admin"));
就新增了Admin
策略,在頁面上只需<AuthorizeView Policy="Admin">
就可以控制只有Admin
策略的賬戶顯示其內容了。
CascadingAuthenticationState
級聯身份狀態,它採用了 Balzor 元件中級聯機制,這樣我們可以在任意層級的元件中使用AuthorizeView
來控制 UI 了
AuthorizeView
元件根據使用者是否獲得授權來選擇性地顯示 UI 內容。
Authorized
元件中的內容只有在獲得授權時顯示。
NotAuthorized
元件中的內容只有在未經授權時顯示。
修改_Imports.razor
檔案,新增必要的引用
@using Microsoft.AspNetCore.Components.Authorization
執行檢視效果
關於更多安全
安全是一個很大的話題,這個章節只是介紹了其最簡單的實現方式,還有更多內容推薦閱讀官方文件:https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/?view=aspnetcore-5.0
次回預告
我們通過幾張圖表,將我們 ToDo 應用中任務情況做個完美統計。
學習資料
更多關於Blazor學習資料:https://aka.ms/LearnBlazor