《进击吧!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 作为身份认证的方式是一个比较好的选择。

基本的使用时序图如下

sequenceDiagram
前端 ->> 服务端: 登录请求
服务端 ->> 服务端:验证身份
服务端 ->> 服务端:创建Token
服务端 -->> 前端: 返回Token
前端 ->> 服务端: 业务请求<br/>包含Token
服务端 ->> 服务端:验证Token
服务端 -->> 前端: 成功

对于安全要求不高的应用采用这个方法简单、易维护,完全没有问题。

但是 Token 本身在安全性上存在以下两个风险:

  1. Token 无法注销,所以可以在 Token 有效期内发送的非法请求,服务端无能为力。
  2. Token 通过 AES 加密存储在客户端,理论上可以进行离线破解,破解后就能任意伪造 Token。

因此遇到安全要求非常高的应用时,我们需要认证服务进行 Token 的有效性验证

sequenceDiagram
前端 ->> 认证服务: 登录请求
认证服务 ->> 认证服务:验证身份
认证服务 ->> 认证服务:创建Token
认证服务 -->> 前端: 返回Token
前端 ->> 服务端: 业务请求<br/>包含Token
服务端 ->>认证服务: 请求验证Token
认证服务 ->> 认证服务:验证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

AuthenticationStateProviderAuthorizeView 组件和 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

《进击吧!Blazor!》系列入门教程 第一章 6.安全的更多相关文章

  1. 《进击吧!Blazor!》系列入门教程 第一章 8.部署

    <进击吧!Blazor!>是本人与张善友老师合作的Blazor零基础入门教程视频,此教程能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力. 视频地址:https://s ...

  2. 《进击吧!Blazor!》系列入门教程 第一章 7.图表

    <进击吧!Blazor!>是本人与张善友老师合作的Blazor零基础入门教程视频,此教程能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力. 视频地址:https://s ...

  3. storm入门教程 第一章 前言[转]

    1.1   实时流计算 互联网从诞生的第一时间起,对世界的最大的改变就是让信息能够实时交互,从而大大加速了各个环节的效率.正因为大家对信息实时响应.实时交互的需求,软件行业除了个人操作系统之外,数据库 ...

  4. storm入门教程 第一章 前言

    转自:http://blog.linezing.com/?p=1847 storm:http://www.cnblogs.com/panfeng412/tag/Storm/ http://blog.l ...

  5. [ABP教程]第一章 创建服务端

    Web应用程序开发教程 - 第一章: 创建服务端 关于本教程 在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发 ...

  6. Laravel 5 系列入门教程(一)【最适合中国人的 Laravel 教程】

    Laravel 5 系列入门教程(一)[最适合中国人的 Laravel 教程] 分享⋅ johnlui⋅ 于 2年前 ⋅ 最后回复由 skys215于 11个月前 ⋅ 17543 阅读   原文发表在 ...

  7. 村田噪声抑制基础教程-第一章 需要EMI静噪滤波器的原因

    1-1. 简介 EMI静噪滤波器 (EMIFIL®) 是为电子设备提供电磁噪声抑制的电子元件,配合屏蔽罩和其他保护装置一起使用.这种滤波器仅从通过连线传导的电流中提取并移除引起电磁噪声的元件.第1章说 ...

  8. [Learn Android Studio 汉化教程]第一章 : Android Studio 介绍

    注:为了看上去比较清晰这里只转载了中文 原地址:  [Learn Android Studio 汉化教程]第一章 : Android Studio 介绍 本章将引导您完成安装和设置开发环境,然后你就可 ...

  9. Storm入门之第一章

    Storm入门之第一章 1.名词 spout龙卷,读取原始数据为bolt提供数据 bolt雷电,从spout或者其他的bolt接收数据,并处理数据,处理结果可作为其他bolt的数据源或最终结果 nim ...

随机推荐

  1. Codeforces Educational Rounds 85 A~C

    A:Level Statistics 题意:统计n个游戏数据,p代表游玩次数,c代表通关次数,每次游玩都不一定通关,求这些数据是否合法 题解:1.游玩次数不能小于通关次数   2.游玩次数和通关次数必 ...

  2. 二、Python基础(input、变量名、条件语句、循环语句、注释)

    一.input用法 input在Python中的含义为永远等待,直到用户输入了值,从而将所输入的值赋值另外的一个东西. n=input('请输入......') 接下来用一个例子学习input的用法 ...

  3. Linux-文件查看命令

    目录 系统文件查看命令-cat 系统文件查看命令-more 系统文件查看命令-less 系统文件查看命令-head 系统文件查看命令-tail 系统文件查看命令-grep 文件上传下载命令-rz,sz ...

  4. docker理论题-02

    1.什么是namespace? 答:名称空间,作用隔离容器 2.namespace隔离有那些? 答:ipc:共享内存.消息队列 mnt:挂载点 net:网络栈 uts:域,主机名 user:用户,组 ...

  5. 一篇文章图文并茂地带你轻松实践 HTML5 history api

    HTML5 history api 前言 由于笔者在网络上没有找到比较好的关于 history api 的实践案例,有的案例过于杂乱,没有重点,有些案例只是告诉读者 api 是什么,却没告诉怎么用,本 ...

  6. 一些简单的SQL语句

    简单的SQL入门 一,简介 1,  一个数据库包含一个或多个表,表包含带有数据的记录(行) 2,  SQL对大小写不敏感,语句的分号看具体情况 二,语法 1,  数据操作语言:DML a)       ...

  7. SPOJ PHRASES Relevant Phrases of Annihilation(后缀数组 + 二分)题解

    题意: 给\(n\)个串,要你求出一个最长子串\(A\),\(A\)在每个字串至少都出现\(2\)次且不覆盖,问\(A\)最长长度是多少 思路: 后缀数组处理完之后,二分这个长度,可以\(O(n)\) ...

  8. 使用 js 实现十大排序算法: 计数排序

    使用 js 实现十大排序算法: 计数排序 计数排序 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!

  9. Apple WWDC All In One

    Apple WWDC All In One https://developer.apple.com/wwdc20/ https://developer.apple.com/videos/wwdc202 ...

  10. Generator function vs Async/Await

    Generator function vs Async/Await generator async /await refs xgqfrms 2012-2020 www.cnblogs.com 发布文章 ...