日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

使用 Blazor 开发内部后台(三):登录

發(fā)布時間:2023/12/4 编程问答 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 使用 Blazor 开发内部后台(三):登录 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

James: 《使用Blazor開發(fā)內(nèi)部后臺》系列是技術(shù)社區(qū)中一位朋友投稿的系列文章,介紹自己為公司的 WebForm 遺留系統(tǒng)使用 Blazor 重寫前端 UI 的經(jīng)歷。

本文為第三篇,如果錯過了前兩篇,建議先閱讀一下:

使用 Blazor 開發(fā)內(nèi)部后臺(一):認(rèn)識Blazor使用?

使用?Blazor 開發(fā)內(nèi)部后臺(二):了解 Blazor 組件


前言

前文為讀者介紹了Blazor及組件的相關(guān)基礎(chǔ)概念,現(xiàn)在讓我們來處理一些實(shí)際的問題。本文將介紹一個簡單的設(shè)計方案:如何基于Blazor開發(fā)內(nèi)部后臺登錄頁面(及相關(guān)模塊)。為了方便初學(xué)者理解正文,本文會先介紹一些工程上必須掌握的基礎(chǔ)知識,有經(jīng)驗(yàn)的開發(fā)者可以選擇性跳過。

托管Blazor WA應(yīng)用(Hosted Blazor Web Assembly)

Blazor WA應(yīng)用可以單獨(dú)部署,稱之為獨(dú)立Blazor WA(Standalone),通常用于(不需要后端的)離線應(yīng)用或者后端服務(wù)基于非ASP.NET?Core的情形。而將Blazor作為ASP.NET?Core應(yīng)用的前端部分一起部署,則被稱為托管Blazor(Hosted)。很顯然,若要開發(fā)一個前后端分離的應(yīng)用,采用托管Blazor,才能最大程度地發(fā)揮Blazor的開發(fā)和部署優(yōu)勢。

項(xiàng)目基本結(jié)構(gòu)

托管Blazor WA應(yīng)用的項(xiàng)目解決方案,主要包含三大子項(xiàng)目:

  • XXX.Client客戶端項(xiàng)目:前端模塊,即Blazor應(yīng)用。

  • XXX.Server服務(wù)端項(xiàng)目:后端模塊,通常是ASP.NET?Core Web API。在最后部署的時候,是由此項(xiàng)目進(jìn)行發(fā)布的,因此該項(xiàng)目會引用Client項(xiàng)目。

  • XXX.Shared類庫項(xiàng)目:共享模塊,主要是存放前后端可以共用的數(shù)據(jù)或邏輯,其他2個項(xiàng)目都要引用它。

而針對Client項(xiàng)目,內(nèi)部也有自己的默認(rèn)結(jié)構(gòu),這里請讀者自行閱讀Blazor項(xiàng)目結(jié)構(gòu)官方文檔,篇幅所限,后文將默認(rèn)讀者已經(jīng)熟悉這些基礎(chǔ)結(jié)構(gòu)。

依賴注入

依賴注入是ASP.NET?Core里一個非常基礎(chǔ)的設(shè)計模式。Blazor里延續(xù)了和后端開發(fā)同樣的風(fēng)格。例如前端向后端發(fā)送請求,需要使用HttpClient,在Program.cs文件里,可以看到:

public class Program{public static async Task Main(string[] args){var builder = WebAssemblyHostBuilder.CreateDefault(args);builder.RootComponents.Add<App>("#app");builder.Services.AddScoped(sp => new HttpClient{BaseAddress = new Uri(builder.HostEnvironment.BaseAddress),Timeout = TimeSpan.FromSeconds(3)});await builder.Build().RunAsync();}}

又例如:我們按照Ant-Design-Blazor項(xiàng)目的《快速上手》說明,引入該開源組件Nuget包后,也需要在這里加上依賴注入的代碼行(其他需要的操作詳見項(xiàng)目文檔):

builder.Services.AddAntDesign();

這對ASP.NET?Core后端開發(fā)者來說,完全沒有理解門檻。而在Page文件里,需要使用HttpClient時,只需要使用@inject關(guān)鍵詞聲明即可:

@inject HttpClient MyHttpClient<div>....... </div>@code{private async Task<string> GetAsync(){string rsp = await MyHttpClient.GetStringAsync(xxxx);return rsp;} }

這里請讀者自行閱讀Blazor依賴注入的官方文檔。對Angular開發(fā)者來說,應(yīng)該也會感到十分親切。

設(shè)計認(rèn)證方式

談到登錄,自然最先要考慮登錄的認(rèn)證方式,常見的有Cookie、Session或Token。對后端渲染的應(yīng)用來說,使用Session應(yīng)該更簡單;而對前后端分離的應(yīng)用來說,后端Web API應(yīng)當(dāng)是無狀態(tài)的,因此一般只選擇Cookie或Token,由前端持有自己的身份票據(jù),后端做驗(yàn)證而不存儲。

而在Cookie和Token之間,我按照官方文檔的建議選擇了使用Json Web Token。這里有必要將官方的理由引用過來,方便讀者參考:

還有對 SPA 進(jìn)行身份驗(yàn)證的其他選項(xiàng),例如使用 SameSite cookie。但是,Blazor WebAssembly 的工程設(shè)計決定,OAuth 和 OIDC 是在 Blazor WebAssembly 應(yīng)用中進(jìn)行身份驗(yàn)證的最佳選擇。出于以下功能和安全原因,選擇了以?JSON Web 令牌 (JWT)?為基礎(chǔ)的基于令牌的身份驗(yàn)證而不是基于 cookie 的身份驗(yàn)證:
使用基于令牌的協(xié)議可以減小攻擊面,因?yàn)椴⒎撬姓埱笾卸紩l(fā)送令牌。
服務(wù)器終結(jié)點(diǎn)不要求針對跨站點(diǎn)請求偽造 (CSRF)?進(jìn)行保護(hù),因?yàn)闀@式發(fā)送令牌。因此,可以將 Blazor WebAssembly 應(yīng)用與 MVC 或 Razor Pages 應(yīng)用一起托管。
令牌的權(quán)限比 cookie 窄。例如,令牌不能用于管理用戶帳戶或更改用戶密碼,除非顯式實(shí)現(xiàn)了此類功能。
令牌的生命周期更短(默認(rèn)為一小時),這限制了攻擊時間窗口。還可隨時撤銷令牌。
自包含 JWT 向客戶端和服務(wù)器提供身份驗(yàn)證進(jìn)程保證。例如,客戶端可以檢測和驗(yàn)證它收到的令牌是否合法,以及是否是在給定身份驗(yàn)證過程中發(fā)出的。如果有第三方嘗試在身份驗(yàn)證進(jìn)程中偷換令牌,客戶端可以檢測被偷換的令牌并避免使用它。
OAuth 和 OIDC 的令牌不依賴于用戶代理行為正確以確保應(yīng)用安全。
基于令牌的協(xié)議(例如 OAuth 和 OIDC)允許用同一組安全特征對托管和獨(dú)立應(yīng)用進(jìn)行驗(yàn)證和授權(quán)。

官方最推薦的方式是使用OAuth和OIDC。但開發(fā)內(nèi)部后臺,還要另搞一個OAuth服務(wù)器,對絕大多數(shù)開發(fā)者來說維護(hù)和部署成本過高了。所以我使用了傳統(tǒng)的Password模式+后端自生成JWT。對內(nèi)部后臺應(yīng)用來說,這么做已經(jīng)足夠安全。

還需要考慮的問題是,前端如何存放JWT呢?我們?nèi)杂袃煞N選擇,Cookie和LocalStorage。如果拿到了JWT放到一個前端自生成的Cookie里……那為什么不一開始就用Cookie呢?顯得有些自我矛盾。我選擇了儲存到LocalStorage里。借助開源項(xiàng)目Blazor.LocalStorage,我們可以很輕松地達(dá)到目的,當(dāng)然,跟Antd一樣要用到依賴注入:

builder.Services.AddBlazoredLocalStorage(config =>{config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;config.JsonSerializerOptions.IgnoreNullValues = true;config.JsonSerializerOptions.IgnoreReadOnlyProperties = true;config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;config.JsonSerializerOptions.WriteIndented = false;});

設(shè)計后端接口

既然已經(jīng)確認(rèn)要使用JWT,那么后端自然要提供一個認(rèn)證的接口:

public class AccountController : ApiControllerBase{private readonly IMemoryCache _cache;private readonly IOptionsMonitor<JwtOption> _jwtOpt;private readonly IPasswordCryptor _passwordCryptor;private readonly MyDbContext _efContext;public AccountController(ILogger<AccountController> logger,IMemoryCache cache,IOptionsMonitor<JwtOption> jwtOpt,IPasswordCryptor passwordCryptor,MyDbContext efContext) : base(logger){_cache = cache;_jwtOpt = jwtOpt;_passwordCryptor = passwordCryptor;_efContext = efContext;}[HttpPost]public async Task<IActionResult> Login([FromForm] LoginRqtDto rqtDto){var cryptedPwd = _passwordCryptor.Encrypt(rqtDto.Password, default);string adminIdCacheKey = CacheKeyHelper.GetAdminIdCacheKey(rqtDto.Account);if (!_cache.TryGetValue(adminIdCacheKey, out int adminId)){adminId = await _efContext.Admins.Where(a => a.Account == rqtDto.Account && a.Password == cryptedPwd).Select(a => a.AdminId).FirstOrDefaultAsync();if (adminId < 1){return Unauthorized();}_cache.Set(adminIdCacheKey, adminId, TimeSpan.FromDays(1));}else{bool checkPwd = await _efContext.Admins.AnyAsync(a => a.AdminId == adminId && a.Password == cryptedPwd);if (!checkPwd){return Unauthorized();}}var claims = new Claim[]{new(ClaimTypes.NameIdentifier, adminId.ToString()),new(ClaimTypes.Name, rqtDto.Account),new(ClaimTypes.Role, "admin")};var jwtSetting = _jwtOpt.CurrentValue;var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.Key));var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);var expiry = DateTime.Now.AddHours(jwtSetting.ExpiryInHours);var token = new JwtSecurityToken(jwtSetting.Issuer, jwtSetting.Audience, claims, expires: expiry, signingCredentials: creds);var tokenText = new JwtSecurityTokenHandler().WriteToken(token);return Ok(tokenText);}}

還需要配置JWT相關(guān)的參數(shù):

"JWT": {"Key": "xxx","Issuer": "xxx","Audience": "xxx","ExpiryInHours": 8}

及依賴注入:

public static IServiceCollection AddAuth(this IServiceCollection services, IConfiguration configuration){services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.TokenValidationParameters = new TokenValidationParameters{ValidateIssuer = true,ValidateAudience = true,ValidateLifetime = true,ValidateIssuerSigningKey = true,ValidIssuer = configuration.GetValue<string>("JWT:Issuer"),ValidAudience = configuration.GetValue<string>("JWT:Audience"),IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetValue<string>("JWT:Key"))),RequireExpirationTime = true};});services.Configure<JwtOption>(configuration.GetSection("JWT"));return services;}

以上代碼僅供讀者參考,可按實(shí)際需要增刪改。另有一句與本文主旨無關(guān)的提醒:雖然是內(nèi)部后臺系統(tǒng),但管理員登錄密碼還是要做加鹽Hash處理,明文保存密碼在任何地方都不可取!

設(shè)計前端服務(wù)

有的讀者可能更喜歡UI先行,那么可以先看下面一節(jié)“設(shè)計登錄頁面”。

有了跟后端一樣的依賴注入,我們可以將前端的認(rèn)證也封裝成服務(wù)。在項(xiàng)目中增加Services文件夾,添加AuthService.cs文件:

using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization;internal class AuthService : IAuthService{private readonly HttpClient _httpClient;private readonly AuthenticationStateProvider _authenticationStateProvider;private readonly ILocalStorageService _localStorage;public AuthService(HttpClient httpClient,AuthenticationStateProvider authenticationStateProvider,ILocalStorageService localStorage){_httpClient = httpClient;_authenticationStateProvider = authenticationStateProvider;_localStorage = localStorage;}public async Task<bool> Login(LoginRqtDto rqtDto){var content = new FormUrlEncodedContent(new KeyValuePair<string, string>[]{new(nameof(LoginRqtDto.Account), rqtDto.Account),new(nameof(LoginRqtDto.Password), rqtDto.Password),});using var rsp = await _httpClient.PostAsync("/account/login", content);if (!rsp.IsSuccessStatusCode){return false;}var authToken = await rsp.Content.ReadAsStringAsync();await _localStorage.SetItemAsync("authToken", authToken);((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(rqtDto.Account);_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);return true;}public async Task Logout(){await _localStorage.RemoveItemAsync("authToken");((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();_httpClient.DefaultRequestHeaders.Authorization = null;}}

首先要注意的是AuthenticationStateProvider,這是一個抽象類,由Microsoft.AspNetCore.Components.Authorization類庫提供,它用來提供當(dāng)前用戶的認(rèn)證狀態(tài)信息。既然是抽象類,我們需要自定義一個它的子類,基于JWT和LocalStorage實(shí)現(xiàn)它要求的規(guī)則(即GetAuthenticationStateAsync方法):

using System.Security.Claims; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Authorization;public class ApiAuthenticationStateProvider : AuthenticationStateProvider{private readonly HttpClient _httpClient;private readonly ILocalStorageService _localStorage;public ApiAuthenticationStateProvider(HttpClient httpClient, ILocalStorageService localStorage){_httpClient = httpClient;_localStorage = localStorage;}public override async Task<AuthenticationState> GetAuthenticationStateAsync(){var savedToken = await _localStorage.GetItemAsync<string>("authToken");if (string.IsNullOrWhiteSpace(savedToken)){return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));}_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedToken);return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));}public void MarkUserAsAuthenticated(string account){var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, account) }, "apiauth"));var authState = Task.FromResult(new AuthenticationState(authenticatedUser));NotifyAuthenticationStateChanged(authState);}public void MarkUserAsLoggedOut(){var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());var authState = Task.FromResult(new AuthenticationState(anonymousUser));NotifyAuthenticationStateChanged(authState);}private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt){var claims = new List<Claim>();var payload = jwt.Split('.')[1];var jsonBytes = ParseBase64WithoutPadding(payload);var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);if (keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles) && roles is string rolesText){if (rolesText.StartsWith('[')){var parsedRoles = JsonSerializer.Deserialize<string[]>(rolesText);foreach (var parsedRole in parsedRoles){claims.Add(new Claim(ClaimTypes.Role, parsedRole));}}else{claims.Add(new Claim(ClaimTypes.Role, rolesText));}keyValuePairs.Remove(ClaimTypes.Role);}claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));return claims;}private static byte[] ParseBase64WithoutPadding(string base64){switch (base64.Length % 4){case 2: base64 += "=="; break;case 3: base64 += "="; break;}return Convert.FromBase64String(base64);}}

邏輯并不復(fù)雜。以上代碼需要讀者對JWT和System.Security.Claims類庫比較熟悉,建議初學(xué)者動手實(shí)踐和調(diào)試。

ILocalStorageService自然是由上文提到的Blazor.LocalStorage類庫依賴注入。

之前系列文章都提到了Blazor在.NET全棧開發(fā)下,具有極大的開發(fā)效率優(yōu)勢。這里就有體現(xiàn)——既然后端已經(jīng)提供了接口,注意到LoginRqtDto類:

using System.ComponentModel.DataAnnotations;public class LoginRqtDto{[Display(Name = "賬號")][Required][StringLength(20, MinimumLength = 3)]public string Account { get; set; }[Display(Name = "密碼")][Required][StringLength(20, MinimumLength = 5]public string Password { get; set; }}

我們自然可以將該類放到Shared項(xiàng)目中,使得前端Blazor項(xiàng)目在調(diào)用Login接口時可以不必再另寫請求參數(shù)的Model。另外,不單單是類本身的屬性,特性也可以被前后端共同利用,這一點(diǎn)放到下文再講。

寫完了該服務(wù),可別忘了依賴注入!我的習(xí)慣是讓Program.cs里的代碼盡可能精簡,因此,我會創(chuàng)建一個Extensions文件夾,添加ServiceCollectionExtension.cs文件:

using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection;internal static class ServiceCollectionExtension{public static IServiceCollection AddAuth(this IServiceCollection services){services.AddAuthorizationCore().AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>().AddScoped<IAuthService, AuthService>();return services;}}

現(xiàn)在只需要在Program.cs里加一行代碼:

builder.Services.AddAuth();

設(shè)計登錄頁面

登錄頁面的獨(dú)特之處,在于布局。例如內(nèi)容頁面是有側(cè)邊導(dǎo)航欄的,但登錄頁面顯然就沒什么必要了。因此,我建議單獨(dú)寫一個LoginLayout組件,和默認(rèn)布局MainLayout分開,只用于Login頁面:

@inherits LayoutComponentBase<Layout Style="padding:0;margin:0"><Header Style="height:10%"><div style="margin:10px;"><AntDesign.Row Justify="space-around" Align="middle"><AntDesign.Col Span="8"><img src="/imgs/logo.png" style="align-self:center" /></AntDesign.Col><AntDesign.Col Span="8" Offset="8" Style="text-align:center"><span style="color:white; font-size:24px">歡迎使用 @ProductionName 后臺管理系統(tǒng)</span></AntDesign.Col></AntDesign.Row></div></Header><Content Style="background-color:white; min-height:500px"><AntDesign.Row><AntDesign.Col Span="20" Offset="2"><div style="margin:100px 0">@Body</div></AntDesign.Col></AntDesign.Row></Content><MyFooter /> </Layout>@code {private const string ProductionName = "Demo"; }

借助于Antd的Layout和Grid組件,可以很輕松地搭建整個Login頁面的布局,這里我采用了最簡單的上中下三層布局。注意到@Body,Body是一種約定命名,表示布局內(nèi)的頁面主體。

對Login頁面來說,@Body其實(shí)就是賬戶輸入、密碼輸入和登錄按鈕。讓我們在Pages文件夾里添加一個Login.razor:

@page "/login" @layout LoginLayout @inject NavigationManager NavigationManager @inject MessageService MsgService @inject IAuthService AuthService<AntDesign.Form Model="@_loginData" Style="height:100%"OnFinish="OnFinish"LabelColSpan="4"WrapperColSpan="4"><FormItem WrapperColOffset="10" WrapperColSpan="4"><AntDesign.Input Placeholder="請輸入賬號" AllowClear="true" @bind-Value="@context.Account"><Prefix><Icon Type="user"></Icon></Prefix></AntDesign.Input></FormItem><FormItem WrapperColOffset="10" WrapperColSpan="4"><InputPassword Placeholder="請輸入密碼" @bind-Value="@context.Password"><Prefix><Icon Type="lock"></Icon></Prefix></InputPassword></FormItem><FormItem WrapperColOffset="11" WrapperColSpan="2"><Button Type="@ButtonType.Primary" HtmlType="submit" Block>登錄</Button></FormItem> </AntDesign.Form>@code {private LoginRqtDto _loginData = new();private async Task OnFinish(EditContext editContext){var result = await AuthService.Login(_loginData);if (!result){await MsgService.Error("帳號或密碼錯誤!");return;}await MsgService.Success("登錄成功!");NavigationManager.NavigateTo("/home");} }

我們使用@layout指令來指定當(dāng)前頁面組件使用哪一種布局;使用Antd提供的Form組件,可以很方便地完成控件布局并添加提交功能;再一次使用LoginRqtDto類,將其屬性與控件的值雙向綁定,實(shí)現(xiàn)最大化代碼復(fù)用;使用依賴注入,在頁面內(nèi)方便地調(diào)用內(nèi)置的NavigationManager和Antd提供的MessageService,分別用于頁面跳轉(zhuǎn)和消息提示。

頁面效果如下:

登錄頁面

依賴于Antd組件的出色實(shí)現(xiàn),諸如密碼的開閉顯示等細(xì)節(jié),都不必我們手動實(shí)現(xiàn)。還有一些細(xì)節(jié)并未在上面的代碼里體現(xiàn)。例如,后端使用System.ComponentModel.DataAnnotations類庫,可以很方便地對接口參數(shù)進(jìn)行校驗(yàn)(如上文提到的LoginRqtDto類)。那么同樣是使用C#,Blazor是否也可以這樣做呢?

當(dāng)然可以!Antd組件同樣利用了接口參數(shù)的校驗(yàn)特性!相較于一般前后端開發(fā),都需要通過API文檔、團(tuán)隊(duì)紀(jì)律和組織溝通,來保證前后端各種數(shù)據(jù)和邏輯的一致性。而使用Blazor開發(fā),在代碼層面就可以天然地讓前后端的行為一致!只要讓定義接口的人將自己的數(shù)據(jù)放到Shared項(xiàng)目里即可。

前端校驗(yàn)提示

(關(guān)于上圖,有過Antd-Blazor開發(fā)經(jīng)驗(yàn)的讀者可能會好奇:這里校驗(yàn)提示為什么是中文而不是默認(rèn)的英文?我將在下文“本地化校驗(yàn)提示”做簡要說明。)

使用AuthorizeView組件動態(tài)顯示內(nèi)容

登錄頁面及服務(wù)設(shè)計好之后,還沒有結(jié)束。對SPA應(yīng)用來說,每個頁面有自己單獨(dú)的路由,用戶可以手動輸入路由繞過登錄頁面來訪問其他頁面。我們理所應(yīng)當(dāng)?shù)叵M绻脩粑吹卿浕蛘J(rèn)證失敗,那么其他頁面對用戶將不提供任何有價值的數(shù)據(jù)。

對后端來說,數(shù)據(jù)相關(guān)的接口都必須加上[Authorize]特性,以校驗(yàn)訪問者的身份。

對前端來說,應(yīng)當(dāng)以友好的方式提示用戶登錄,而不是依舊發(fā)送頁面請求,依賴后端接口返回401或403再手動處理。

MainLayout和AuthorizeView組件可以幫助我們統(tǒng)一處理這種情況。

使用AuthorizeView組件之前,我們需要在App.razor文件里,使用CascadingAuthenticationState組件包裹Router組件:

@using Microsoft.AspNetCore.Components.Authorization<CascadingAuthenticationState><Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"><Found Context="routeData"><AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /></Found><NotFound> <MyNotFound /></NotFound></Router> </CascadingAuthenticationState><AntContainer />

然后在MainLayout的Content部分使用AuthorizeView組件:

<Content Style="background-color:white; min-height:500px"><AuthorizeView><Authorized>@Body</Authorized><NotAuthorized><div style="margin: 100px 0; width:100%; text-align: center; color: red;"><span style="font-size:20px">檢測到登錄超時,請重新<a href="/login" style="text-decoration:underline">登錄</a>!</span></div></NotAuthorized></AuthorizeView><BackTop></BackTop></Content>

單從標(biāo)簽命名上看就很容易理解:認(rèn)證通過則顯示@Body的內(nèi)容,否則顯示一行字提示用戶訪問登錄頁。讓我們看下不登錄情況下直接訪問Home首頁的效果:

NotAuthorized時的Content

這樣,對于默認(rèn)使用MainLayout布局的其他所有頁面,若用戶未認(rèn)證,則只會顯示上圖的效果。同理,我們可以實(shí)現(xiàn)布局的Header部分動態(tài)顯示:未認(rèn)證情況下,不應(yīng)顯示上方“首頁/關(guān)于”導(dǎo)航欄和右上方的賬號信息,這里本文不再贅述。

本地化校驗(yàn)提示

至此本文核心內(nèi)容都已經(jīng)結(jié)束了。但在編寫登錄頁面的過程中,有一個細(xì)節(jié)值得一提。

在設(shè)計登錄頁面一節(jié)中,我提到了前端校驗(yàn)提示。目前Antd組件在校驗(yàn)提示上,還是使用System.ComponentModel.DataAnnotations類庫的默認(rèn)提示:提示是全英文的。

在上文提到的LoginRqtDto中,我們可以使用Display特性,來修改校驗(yàn)失敗提示時屬性的展示名稱。但并不能修改整個提示的內(nèi)容,因此讀者只會看到中英文混合的一段提示文本。

注意到校驗(yàn)特性的父類ValidationAttribute,有ErrorMessageResourceName和ErrorMessageResourceType兩個屬性。也就是說該父類在設(shè)計上,是支持本地化的,我們可以創(chuàng)建Resource資源,來替換類庫默認(rèn)的錯誤提示。

在XXX.Shared項(xiàng)目中,創(chuàng)建Resources文件夾,添加一個DA_zh_CN.resx文件(命名隨意):

中文提示資源

IDE VS會自動生成一個的DA_zh_CN.designer.cs文件,為你創(chuàng)建DA_zh_CN類。

將上文提到的LoginRqtDto改為:

public class LoginRqtDto{[Display(Name = "賬號")][Required(ErrorMessageResourceName = "RequiredError", ErrorMessageResourceType = typeof(Resources.DA_zh_CN))][StringLength(20, MinimumLength = 3, ErrorMessageResourceName = "StringLengthError_IncludingMin", ErrorMessageResourceType = typeof(Resources.DA_zh_CN))]public string Account { get; set; }[Display(Name = "密碼")][Required(ErrorMessageResourceName = "RequiredError", ErrorMessageResourceType = typeof(Resources.DA_zh_CN))][StringLength(20, MinimumLength = 5, ErrorMessageResourceName = "StringLengthError_IncludingMin", ErrorMessageResourceType = typeof(Resources.DA_zh_CN))]public string Password { get; set; }}

好了,收工。這里resx文件里“名稱”列,我也不是隨意取的,而是照搬官方源碼里的名稱。有興趣的讀者可以參閱System.ComponentModel.DataAnnotations類庫的相關(guān)源碼。

我也希望未來能有更簡單的方式實(shí)現(xiàn)控件本地化校驗(yàn)提示。

結(jié)束語

下一篇文章會簡單許多,我將介紹如何使用Antd的Card組件和優(yōu)雅的Razor語法,做一個可靈活配置的、用于導(dǎo)航的首頁。再會!

總結(jié)

以上是生活随笔為你收集整理的使用 Blazor 开发内部后台(三):登录的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。