《进击吧!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