《進擊吧!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-ToDohtml
做者:陳超超
Ant Design Blazor 項目貢獻者,擁有十多年從業經驗,長期基於.Net 技術棧進行架構與開發產品的工做,現就任於正泰集團。
郵箱:timchen@live.com
歡迎各位讀者有任何問題聯繫我,咱們共同進步。前端
個人的 ToDo 應用基本功能已經完成,可是本身的待辦固然只有本身知道,因此咱們此次給咱們的應用增長一些安全方面的功能。git
Blazor Server 應用和 Blazor WebAssembly 應用的安全方案有所不一樣。程序員
Blazor WebAssembly 應用在客戶端上運行。 因爲用戶可繞過客戶端檢查,由於用戶可修改全部客戶端代碼, 所以受權僅用於肯定要顯示的 UI 選項,全部客戶端應用程序技術都是如此。github
Blazor Server 應用經過使用 SignalR 建立的實時鏈接運行。 創建鏈接後,將處理基於 SignalR 的應用的身份驗證。 可基於 cookie 或一些其餘持有者令牌進行身份驗證。後端
AuthorizeView
組件根據用戶是否得到受權來選擇性地顯示 UI 內容。 若是隻須要爲用戶顯示數據,而不須要在過程邏輯中使用用戶的標識,那麼此方法頗有用。api
<AuthorizeView> <Authorized> <!--驗證經過顯示--> </Authorized> <NotAuthorized> <!--驗證不經過顯示--> </NotAuthorized> </AuthorizeView>
在 Blazor WebAssembly 模式下, 由於應用都在客戶端運行,因此使用 Token 做爲身份認證的方式是一個比較好的選擇。
基本的使用時序圖以下安全
對於安全要求不高的應用採用這個方法簡單、易維護,徹底沒有問題。服務器
可是 Token 自己在安全性上存在如下兩個風險:cookie
所以遇到安全要求很是高的應用時,咱們須要認證服務進行 Token 的有效性驗證
接着咱們對以前的 ToDo 項目進行改造,讓他支持登陸功能。
先把先後端交互所需的 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; } }
先改造服務端,添加必要引用,編寫身份認證代碼等
添加 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 的密鑰,規則等,實際項目時能夠將這些信息放到配置中。
行政驗證控制器,用於驗證用戶身份,建立 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); } }
改造客戶端,讓客戶端支持身份認證
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