《进击吧!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. Educational DP Contest E - Knapsack 2 (01背包进阶版)

    题意:有\(n\)个物品,第\(i\)个物品价值\(v_{i}\),体积为\(w_{i}\),你有容量为\(W\)的背包,求能放物品的最大价值. 题解:经典01背包,但是物品的最大体积给到了\(10^ ...

  2. vs2019激活码

    Visual Studio 2019 Enterprise BF8Y8-GN2QH-T84XB-QVY3B-RC4DF Visual Studio 2019 Professional NYWVH-HT ...

  3. Dapr微服务应用开发系列1:环境配置

    题记:上篇Dapr系列文章简要介绍了Dapr,这篇来谈一下开发和运行环境配置 本机开发环境配置 安装Docker 为了方便进行Dapr开发,最好(其实不一定必须)首先在本机(开发机器)上安装Docke ...

  4. Redis 主从复制(Replication)

    为了保证服务的可用性,现代数据库都提供了复制功能,同时在多个进程中维护一致的数据状态. Redis 支持一主多从的复制架构,该功能被简化成了一条 SLAVEOF 命令,下面通过条命令来解析 Redis ...

  5. Programming Interview Questions Websites All In One

    Programming Interview Questions Websites All In One 编程面试刷题网站 http://highscalability.com/ https://tri ...

  6. ECharts Pie All In One

    ECharts Pie All In One 饼图 https://echarts.apache.org/examples/zh/index.html#chart-type-pie 嵌套饼图 http ...

  7. 2016 JS 笔试题汇总:

    1 1 1 CS&S(中软国际): 1 JavaScript 循环表达式: 2  JavaScript表达式boolean返回值: 3 网页中的事件/HTML 事件属性/JavaScript ...

  8. swiper & swiper slider

    swiper & swiper slider mobile swiper https://idangero.us/swiper/ https://idangero.us/swiper/get- ...

  9. uniapp 修改meta:viewport

    onLoad(options) { this.setViewport(`width=device-width, initial-scale=1.0`); }, onUnload() { this.se ...

  10. NGK公链:在规则明确的环境下运行超级节点机制

    首先要跟大家明确的一点是,21个超级节点是投票选举出来的,并不是系统在创立之初就已经确定好了的.那么相信大家也一定很好奇,这21个超级节点是通过什么方式产生? NGK.IO对分布式超级节点使用了一个自 ...