背景
近期在开发一个全新的管理系统,使用了微软全新的Blazor框架,开发到现在,越发觉得Blazor的设计非常巧妙,虽然它的特点是使用C#语言代替了Javascript来开发web页面中的逻辑部分,统一前后开发的技术栈,但我觉得这根本不是重点。对于想做全栈开发的.Net开发者来说,一下子跳转到Vue,React等前端开发框架的话,尤其在开发一些稍微有一点规模的项目时,需要频繁的切换开发模式,这包括你的思维要不断在前后端环境跳跃,使用的编辑器应该也会不一样,毕竟对于Vue这类开发框架,使用VS或者Rider这类IDE并不是非常好用,当然这不是什么大问题,但这一定会折损一些开发效率。尤其当你全神贯注在实现一个功能模块,需要前后对接调试的时候,还是会期望有一个更高效的开发环境。
而Blazor的价值就在于,他并不是要替代Vue,React,而是针对性的给致力于做全栈开发的.Net开发者们多一个"优"选项,这一点,如果你真正进行过中度以上的体验,才会发现,Blazor真非常好,属于那种漫热型的框架,越用越上头,它不是一个平替选项,而是完全值得开发者深度投入的开发框架,它模块化的设计思路,也和Vue,React等框架的设计思路不谋而合,使你不用太顾虑被.Net技术栈套牢,因为如果能很好的驾驭Blazor的话,后续即便是要跳到Vue这些框架,这个技术迁移成本也不会太大,当然反过来也一样。这才是Blazor最有价值的地方!有独立个性,却又兼容并包,像个成熟的大人,值得信赖!
哈哈,说多来,接下来,我将通过一个实际的小模块案例,带你领略Blazor的魅力。
注意: 关于Blazor的详细教程和深度学习,强烈推荐微软官方的教程和文档。本文仅是我结合实际项目经验,从特定场景出发,抛砖引玉,希望能为Blazor社区生态贡献一份力量。
案例
这里,我以现在项目中的一个管理模块为例,介绍下一个小功能模块的增删改查的开发过程。
注意,下面我给出的案例代码会进行一定的删减和调整,去除掉业务相关的部分
布局组件
Blazor的开发模式和Vue的组件式开发模式很像,也是模块化的,这里我们准备一个布局组件,然后其他组件都可以使用它
csharp
// 这里要引入一些命名空间,也可以将常用的命名空间放到_Imports.razor里,我这里不再展示
@inherits LayoutComponentBase
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<MudAppBar Elevation="1">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start"
OnClick="@((e) => DrawerToggle())" />
<MudText Typo="Typo.h5" Class="ml-3">综合信息管理平台(孵化期🐣)</MudText>
<MudBadge Icon="@Icons.Material.Filled.History" Color="Color.Tertiary" Overlap="true" Bordered="true" Class="mx-6 my-4">
<MudButton Target="_blank" Href="https://www.yuque.com/yuqueyonghuhz99ts/iw926p/cgkrocxvego2aydx?singleDoc#hPlWN">开发进度</MudButton>
</MudBadge>
<MudSpacer />
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle" />
<AuthorizeView>
<Authorized>
<MudMenu AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopRight">
<ActivatorContent>
<MudAvatar Size="Size.Small" Class="ml-2">
<MudIcon Icon="@Icons.Material.Filled.Person" />
</MudAvatar>
</ActivatorContent>
<ChildContent>
<MudText Typo="Typo.body2" Class="pa-2">
<Magic.Declaration.Manage.Components.Shared.UserInfoDisplay ShowType="1" />
</MudText>
<MudDivider />
<Magic.Declaration.Manage.Components.Shared.LogoutButton></Magic.Declaration.Manage.Components.Shared.LogoutButton>
</ChildContent>
</MudMenu>
</Authorized>
</AuthorizeView>
</MudAppBar>
<MudDrawer id="nav-drawer" @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
<NavMenu />
</MudDrawer>
<MudMainContent Class="pt-16 pa-4">
@Body
</MudMainContent>
</MudLayout>
<div id="blazor-error-ui" data-nosnippet>
页面出错.
<a href="." class="reload">重载</a>
<span class="dismiss">🗙</span>
</div>
@code {
//省略
//这里主要的逻辑是控制页面主题变化,代码篇幅较长不再展示
}
简单说明一下,这个布局页面,包含了一个顶部的导航栏,一个侧边菜单栏和主要内容的管理区域,另外组件内有一些通用命名空间的引入,可以放到Imports.razor组件中,这样就不用在页面里写许多引用代码了。
效果如下

正如上图中展示的一样,我的开发习惯是准备开发一个业务模块之前,先把底层要用到的服务准备好,比如读操作,写操作,模型关联等等,然后准备好接口,之后就可以去做页面开发了。注意本篇不会介绍Razor页面开发之外的内容。
进入列表开发环节后,我们看到我需要一些检索操作区域,比如按一定的条件来检索,需要一些功能按钮,比如导出,编辑等等。
列表页
面包屑组件
这里从上到下说,列表页最上面有一个面包屑,这个是可以开发成一个统一的组件,然在不同的页面导入就好
csharp
<MudBreadcrumbs Items="_items"></MudBreadcrumbs>
<MudDivider Style="padding:10px" />
@code {
[Parameter]
public string PageTitle { get; set; } = "菜单";
[Parameter]
public string PageMain { get; set; } = "menu";
[Parameter]
public string PageSub { get; set; } = "create";
private string PageSubTitle
{
get
{
//业务处理代码,省略
}
}
protected override void OnInitialized()
{
base.OnInitialized();
_items.Add(new BreadcrumbItem(PageTitle, $"/{PageMain}/index"));
_items.Add(new BreadcrumbItem(PageSubTitle, $"/{PageMain}/{PageSub}"));
}
private List<BreadcrumbItem> _items { get; set; } = [
new ("首页", "/"),
];
}
这样一个通用组件就准备好了,在用到地方导入即可。
筛选框组件
我这里列表页里的筛选框也是一个全局的筛选框,包括一个状态选择和一个业务相关的筛选,这里我以那个状态选择为例
csharp
<MudSelect T="ManagerStatusFilter" Label="状态" Variant="Variant" ValueChanged="OnStatusChanged">
@foreach (var status in Enum.GetValues(typeof(ManagerStatusFilter)).Cast<ManagerStatusFilter>())
{
<MudSelectItem Value="@status">@EnumExtensions.GetDescription(status)</MudSelectItem>
}
</MudSelect>
@code {
[Parameter]
public ManagerStatusFilter Status { get; set; }
[Parameter]
public Variant Variant { get; set; } = Variant.Outlined;
[Parameter]
public EventCallback<ManagerStatusFilter> StatusChanged { get; set; }
private async Task OnStatusChanged(ManagerStatusFilter value)
{
//执行回调方法
await StatusChanged.InvokeAsync(value);
}
}
这样两个通用组件就封装好了,组件代码里的"code"部分定义的变量,如果标记了Parameter特性,则该参数是可以从调用页传值进来的,而传值也不仅限于参数,也包括委托事件,进而实现了组件之间的数据通信。
比如我在列表组件代码里调用这两个组件的代码
csharp
//调用面包屑组件
<MagicBreadcrumbs PageTitle="元数据配置" PageMain="dictmetaconfig" PageSub="index" />
//调用筛选框组件
//这个组件需要传入一个委托方法,监控状态的变化,而状态值是从子组件内传过来的
<FilterSelect StatusChanged="HandleStatusChanged"></FilterSelect>
这两个组件,第一个面包屑组件的调用比较简单,直接把组建名以类似Html元素的形式写入,然后后面的元素属性就是参数,按需赋值即可。
而筛选框组件,需要传入回调方法,监控状态的变化,而状态值是从子组件里传过来的,所以要在回调方法里获取这个值
csharp
private async Task HandleStatusChanged(ManagerStatusFilter status)
{
filterBuilder.RemoveCondition("Status");
if (status != ManagerStatusFilter.UnKnown)
{
filterBuilder.AddSingle("Status", status);
}
await table.ReloadServerData();
}
上面这个方法,直接以参数的方式给到组件就ok了,这样就可以完成组件件的传值。
列表页其他内容
列表页主要的呈现就是那个table元素,这个在加载之前,要先从服务端把table数据获取过来,前后分离的方式这一步是要发送一个Ajax请求来完成,而在Blazor里,就可以像写接口一样,简单调用就可以了
Code部分的关键代码如下
csharp
@inject IDictMetaConfigProvider _dictMetaConfigProvider //注入服务,这个也可以写到code块里
@inject ISnackbar Snackbar
@code{
private DynamicFilterBuilder filterBuilder = new DynamicFilterBuilder();
private List<DictMetaConfigResponseDto> dictMetaConfigs = new();
private MudTable<DictMetaConfigResponseDto> table = new();
public async Task<TableData<DictMetaConfigResponseDto>> LoadDictMetaConfigAsync(TableState state, CancellationToken ct)
{
var pageRequest = new PageReqDto
{
whereJsonStr = filterBuilder.BuildJson(),
pageindex = state.Page + 1,
pagesize = state.PageSize,
orderby = "a.createdat",
isAsc = true
};
var result = await _dictMetaConfigProvider.GetMetaConfigPageList(pageRequest, ct);
if (!result.IsSuccess)
{
Snackbar.Add(result.ErrorMessage, Severity.Warning);
return new();
}
dictMetaConfigs = result.Value;
int total = Convert.ToInt32(result.Total);
return new TableData<DictMetaConfigResponseDto>()
{
TotalItems = total,
Items = dictMetaConfigs
};
}
}
这段就和写Api接口比较像了,注入服务之后,获取数据即可。这里面还涉及到一些页面交互组件,比如没有数据的时候有提示等等,整个过程都是用C#代码完成的,而Blazor会通过SignalR框架将数据传送到浏览器,完成数据的渲染。
页面部分的主要代码如下
csharp
<MudTable
ServerData="LoadDictMetaConfigAsync"
Hover="true"
Bordered="true"
Breakpoint="Breakpoint.Md"
Style="width:-webkit-fill-available"
@bind-SelectedItems="selectedItems"
FixedHeader="true"
SelectOnRowClick="_selectionChangeable"
SelectionChangeable="true"
MultiSelection="true"
T="DictMetaConfigResponseDto"
OnRowClick="@OnRowClick"
@ref="table">
<ToolBarContent>
<MudText Typo="Typo.h6">元数据</MudText>
<MudSpacer />
<MudTextField T="string" ValueChanged="@(s => OnSearch(s))" Placeholder="输入名称后回车检索" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</ToolBarContent>
<HeaderContent>
<MudTh>属性名称</MudTh>
<MudTh>属性说明</MudTh>
<MudTh>属性类型</MudTh>
<MudTh>是否必填</MudTh>
<MudTh>归属模型</MudTh>
<MudTh>状态</MudTh>
<MudTh>操作</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="属性名称">@context.JsonKey</MudTd>
<MudTd DataLabel="属性说明">@context.BusinessName</MudTd>
<MudTd DataLabel="属性类型">
@context.FieldType.GetDescription()
</MudTd>
<MudTd DataLabel="是否必填">
@{
if (context.IsRequired)
{
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Secondary">必填</MudChip>
}
else
{
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Default">非必填</MudChip>
}
}
</MudTd>
<MudTd DataLabel="归属模型">@context.JoinedTitle</MudTd>
<MudTd DataLabel="状态">
@{
if (context.Status == StatusEnum.Enabled)
{
<MudChip T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">可用</MudChip>
}
else
{
<MudChip T="string" Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Default">不可用</MudChip>
}
}
</MudTd>
<MudTh>
<MudButton Href="@($"/dictmetaconfig/edit/{context.Id}")"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Edit">
编辑
</MudButton>
<MudButton onclick="(() => HandleDeleteClick(context.Id))"
Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Delete">
删除
</MudButton>
</MudTh>
</RowTemplate>
<NoRecordsContent>
<MudText>无记录</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
这里主要的部分就是在Table组件里,使用ServerData属性绑定数据加载的事件"LoadDictMetaConfigAsync",然后在RowTemplate元素中,进行模型绑定,和之前使用js进行开发的流程基本是一样的。
这一套流程完成后,我们差不多就得到了这样一个页面

受篇幅限制,我没有把所有的地方都写出来,只展示了必要的组件,和表格数据获取的过程,其他的部分还是跟业务相关,大家结合微软的文档自行探索实现即可。
表单页
这里表单页实际上也可以根据场景来做一定的封装,将表单封装成一个当前业务模块的专属组件,然后创建页和编辑页就都可以用它了。
当然这个是分场景的,并不是所有组件都适合这样做。
表单组件
我这里的表单要实现模型的创建和修改,当创建场景时,它就是一个空表单,表单元素是需要用户输入或者选择,而编辑场景,需要默认把表单渲染好,然后用户进行修改即可。
组件代码如下
csharp
@using FluentValidation
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@inject Magic.Declaration.Application.Interfaces.Manage.IDecMainRequirementProvider _decMainRequirementProvider
<MudContainer MaxWidth="MaxWidth.Medium">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h4">@(Id.HasValue ? "编辑活动要求" : "添加活动要求")</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudForm Model="@model" @ref="@form" Validation="@(requriementValidator.ValidateValue)" ValidationDelay="300">
<MudTextField @bind-Value="model.Title"
For="@(() => model.Title)"
Immediate="true"
Label="要求标题"
FullWidth="true"
Margin="Margin.Normal" />
<MudTextField @bind-Value="model.Description"
For="@(() => model.Description)"
Placeholder="请填写详细的活动说明"
AutoGrow
Label="要求说明"
FullWidth="true"
Margin="Margin.Normal" />
<MudSelect @bind-Value="model.DecMainRequirementType"
Label="申报成员类型"
AdornmentIcon="@Icons.Material.Outlined.Tag"
HelperText="如学生,站点,合作单位等个体,组织等"
AdornmentColor="Color.Tertiary">
@foreach (var item in Enum.GetValues(typeof(Domain.DecMainRequirementType)).Cast<Domain.DecMainRequirementType>())
{
<MudSelectItem Value="@item">@EnumExtensions.GetDescription(item)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="model.MaxQuantity"
For="@(() => model.MaxQuantity)"
Label="数值上限"
Immediate="true"
InputType="InputType.Number"
FullWidth="true"
OnlyValidateIfDirty="true"
Margin="Margin.Normal" />
<MudTextField @bind-Value="model.MinQuantity"
For="@(() => model.MinQuantity)"
Immediate ="true"
Label="数值下限"
InputType="InputType.Number"
FullWidth="true"
OnlyValidateIfDirty="true"
Margin="Margin.Normal" />
<MudSelect Label="状态" @bind-Value="model.Status" FullWidth="true" Margin="Margin.Normal">
@foreach (var status in Enum.GetValues(typeof(Domain.StatusEnum)).Cast<Domain.StatusEnum>())
{
<MudSelectItem Value="@status">@EnumExtensions.GetDescription(status)</MudSelectItem>
}
</MudSelect>
@{
if (Id.HasValue)
{
<DecMainSelect Variant="Variant.Text" NodeId="model.DecMainId"></DecMainSelect>
}
else
{
<DecMainSelect Variant="Variant.Text" MainIdChanged="HandleMainIdChanged"></DecMainSelect>
}
}
</MudForm>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Submit" Class="ml-auto">保存</MudButton>
<MudButton Href="/decmainrequirement/index" Variant="Variant.Text" Color="Color.Default">返回</MudButton>
</MudCardActions>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
[Parameter] public long? Id { get; set; }
MudForm form=new ();
DecMainRequirementRequestDto model = new DecMainRequirementRequestDto();
RequirementModelFluentValidator requriementValidator = new RequirementModelFluentValidator();
protected override async Task OnInitializedAsync()
{
if (!Id.HasValue)
{
return;
}
var result = await _decMainRequirementProvider.GetOneAsync(u => u.Id == Id);
if (result == null)
{
return;
}
model = result.Adapt<DecMainRequirementRequestDto>();
}
private void HandleMainIdChanged(DecMain.DecMainSelect.MainSelectObj mainSelect)
{
if (mainSelect.IsLeaf)
model.DecMainId = mainSelect.Id;
}
private async Task Submit()
{
await form.Validate();
if (!form.IsValid)
{
Snackbar.Add("表单验证失败!",MudBlazor.Severity.Error);
return;
}
var result = await _decMainRequirementProvider.CreateOrModifyRequirement(model);
if(!result.IsSuccess){
Snackbar.Add(result.ErrorMessage, MudBlazor.Severity.Error);
return;
}
Snackbar.Add("保存成功", MudBlazor.Severity.Success);
Navigation.NavigateTo("/decmainrequirement/index");
}
/// <summary>
/// 尝试一下表单验证,后续其他复杂表单可以以此为例,进行扩充调整
/// </summary>
public class RequirementModelFluentValidator : AbstractValidator<DecMainRequirementRequestDto>
{
public RequirementModelFluentValidator()
{
RuleFor(m => m.Title)
.NotEmpty()
.Length(1, 50);
RuleFor(m => m.Description)
.MaximumLength(1500);
RuleFor(m => m.MaxQuantity)
.NotEmpty()
.GreaterThan(0)
.GreaterThanOrEqualTo(m => m.MinQuantity);
RuleFor(m => m.MinQuantity)
.NotEmpty()
.GreaterThan(0)
.LessThanOrEqualTo(m => m.MaxQuantity);
RuleFor(m => m.DecMainRequirementType)
.NotEqual(Domain.DecMainRequirementType.None);
}
#region 如果验证规则不满足,这里还可以定义私有方法,然后进行配置如
// 规则包到👆上面的构造函数里去
// RuleFor(x => x.Email)
// .Cascade(CascadeMode.Stop)
// .NotEmpty()
// .EmailAddress()
// .MustAsync(async (value, cancellationToken) => await IsUniqueAsync(value));
// 私有方法写外面
// private async Task<bool> IsUniqueAsync(string email)
// {
// // Simulates a long running http call
// await Task.Delay(2000);
// return email.ToLower() != "test@test.com";
// }
#endregion
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<DecMainRequirementRequestDto>.CreateWithOptions((DecMainRequirementRequestDto)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
}
表单的逻辑比较简单,这里需要说明的就是引入了FluentValidation插件进行了表单验证,这不是必须的,如果你有其他的表单验证方式,可以不使用这个
创建页
表单组件创建好后,就非常简单了,在创建页和编辑页就像常规的组件调用一样
csharp
@page "/DecMainRequirement/create"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<MagicBreadcrumbs PageTitle="活动要求" PageMain="DecMainRequirement" PageSub="create" />
<InsertOrUpdateForRequirement></InsertOrUpdateForRequirement>
这里第一行是类似一个路由定义的代码,就可以通过这个地址访问到这个页面了,当然通用组件里不需要有这一步
页引入了一些认证授权的东西,这不是本篇重点,不在介绍。下面行就是面包屑组件和我们定义的表单组件了。

编辑页
编辑页和创建页不同的是,要传入一个id,这个id在表单页会发生作用,检索到当前的数据实体并完成表单赋值
csharp
@page "/DecMainRequirement/edit/{Id:long}"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<MagicBreadcrumbs PageTitle="活动要求" PageMain="DecMainRequirement" PageSub="edit" />
<InsertOrUpdateForRequirement Id="@Id"></InsertOrUpdateForRequirement>
@code {
[Parameter]
public long? Id { get; set; }
}
这个和Create页的不同点就是引入表单组件的时候,传入了id,而这个id也是从其他页面传入的,比如刚刚我们定义的列表页,而头部的路由也使用了Rest风格,将id拼在了URL路径里。
这样处理完后,再次进入之后,表单元素会被赋好值,页面效果如下

删除
我这里的删除,就是在列表页增加了一个删除按钮,当然这仅限于单条的删除,如果涉及到批量删除的话,可能还需要做特殊的设计
csharp
private async Task HandleDeleteClick(long Id)
{
var options = new DialogOptions { CloseOnEscapeKey = true };
var parameters = new DialogParameters
{
["ContentText"] = "确定要删除这条记录吗?此操作不可恢复。",
["ButtonText"] = "删除",
["Color"] = Color.Error
};
var dialog = await DialogService.ShowAsync<DialogConfirm>("确认删除", parameters, options);
var result = await dialog.Result;
if (result == null || result.Canceled)
{
return;
}
try
{
await ValidateAntiforgeryTokenAsync();
var removeResult = await _dictMetaConfigProvider.RemoveMetaConfig(Id);
if (removeResult.IsSuccess)
{
Snackbar.Add(removeResult.ErrorMessage, Severity.Info);
await table.ReloadServerData();
return;
}
Snackbar.Add($"删除失败:{removeResult.ErrorMessage}", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
}
}

至此,一个功能模块的基本管理功能就完成了,整个过程,的确不需要任何的JavaScript知识,甚至CSS知识也不需要,我这里是使用了MudBlazor,他本身就基于Metarial风格封装了一些样式,然后使用起来和TailwindCSS一样,约定俗成,拿来用就行,这也和那些AntDesgin,TDesign等框架一样。
*安全性
接下来,我想在简单聊一下Blazor在安全性上的独特能力,以Blazor Server为例,它的数据托管模型是下图这样

更多关于Blazor托管模型的说明👉:learn.microsoft.com/zh-cn/aspne...
因为BlazorServer在服务器端渲染UI,并通过SignalR实时通信更新客户端。这意味着所有的业务逻辑都在服务器端执行,客户端只接收到渲染后的HTML和必要的UI更新指令。这种架构天然地免疫了许多常见的Web攻击,例如跨站脚本攻击(XSS)。由于没有直接在客户端执行的JavaScript代码,恶意脚本注入的风险大大降低。此外,Blazor Server也能方便地集成ASP.NET Core的内置安全机制,如防范跨站请求伪造(CSRF)攻击,为您的应用提供更坚固的安全保障。
增加防伪和数据保护配置
为了增强Blazor的数据安全能力,比如防止CSRF攻击的配置和配置数据防护,我们可以配置相关的服务
csharp
//定义一个私有方法,单独进行数据安全层面的配置
private static void ConfigureDataProtection(this IServiceCollection services, IConfiguration configuration)
{
if (configuration.GetSection("RedisSentinelStr").Exists())
{
services.AddDataProtection()
.PersistKeysToStackExchangeRedis(
ConnectionMultiplexer.Connect(configuration.GetSection("RedisSentinelStr").Value),
"DataProtection-Keys");
}
services.AddAntiforgery(options =>
{
options.Cookie.Name = "X-XSRF-TOKEN";
options.Cookie.Path = "/";
options.HeaderName = "RequestVerificationToken";
});
services.AddSingleton<IAntiforgeryTokenService, AntiforgeryTokenService>();
}
//然后注入服务
builder.Services.ConfigureDataProtection(_configuration);
注入的防伪服务代码
csharp
public class AntiforgeryTokenService : IAntiforgeryTokenService
{
private readonly IAntiforgery _antiforgery;
private readonly IHttpContextAccessor _httpContextAccessor;
public AntiforgeryTokenService(IAntiforgery antiforgery, IHttpContextAccessor httpContextAccessor)
{
_antiforgery = antiforgery;
_httpContextAccessor = httpContextAccessor;
}
public string GetRequestToken()
{
if(_httpContextAccessor.HttpContext == null)
return string.Empty;
var tokens = _antiforgery.GetAndStoreTokens(_httpContextAccessor.HttpContext);
return tokens.RequestToken??string.Empty;
}
}
然后我们还是像封装Blazor组件一样,封装一个防伪组件
csharp
using Magic.Declaration.Manage.Services;
using Microsoft.AspNetCore.Components;
namespace Magic.Declaration.Manage.Components.SmallComponents
{
public partial class AntiforgeryFormBase : ComponentBase
{
[Inject] protected IAntiforgeryTokenService? AntiforgeryTokenService { get; set; }
protected string? AntiforgeryToken { get; private set; }
protected override async Task OnInitializedAsync()
{
// 获取并存储防伪 Token
AntiforgeryToken = AntiforgeryTokenService?.GetRequestToken();
Console.WriteLine("防伪码:" + AntiforgeryToken);
await base.OnInitializedAsync();
}
/// <summary>
/// 在子类中调用此方法来验证当前请求的防伪 Token
/// </summary>
protected async Task ValidateAntiforgeryTokenAsync()
{
// 这里不需要手动验证 Token,因为 ASP.NET Core 已经自动处理了
// 我们只需要确保 Token 被正确地附加到了请求中
await Task.Delay(1);
}
}
}
然后在Blazor组件中,直接注入使用即可
csharp
private async Task HandleValidSubmit()
{
try
{
//加入这一行👇
await ValidateAntiforgeryTokenAsync();
var result = await _decMainProvider.CreateOrUpdateMain(mainDto);
if (!result.IsSuccess)
{
Snackbar.Add("活动保存失败!" + result.ErrorMessage, Severity.Error);
return;
}
Snackbar.Add("活动保存成功!", Severity.Success);
Navigation.NavigateTo("/decmain/index");
}
catch (Exception ex)
{
// 处理异常
Snackbar.Add("活动保存失败!" + ex.Message, Severity.Error);
}
}
这样操作完成后,就像在MVC的views里一样,所有的请求就都会携带一个这样的防伪标识,进一步提高系统的安全防护能力

burp拦截的交互请求

打开Burp,我们可以看一下它的数据交互情况,可以看到系统建立许多ws的链接历史,而这里实际上是一个长链接,如下图

这里connect那个是ide建立的做热重载用的,实际生产环境环境不会有这个套接字,实际产生数据交互,也就是服务端控制web端页面变化的套接字就是_blazor的那个,我们可以打开burp看一下它的请求历史,全部都是加密之后的字符,也就是说,这整个的数据交互过程都是安全的。

作为对比,我们拦截一个基于前后分离,通过http协议进行数据交互的例子

可以看到,尽管网站使用了https协议,但其数据交互信息,在数据发起这边,依旧是明文显示的,也就是你最多只能确保在网络传输层的安全,而黑客依旧是可以通过这次网络抓包工具,完整的获取你的请求参数,并进行一些破坏性的操作。而BlazorServer就不会,你通过抓包工具获取到的信息,也是加密的,这样就提高了数据的安全性。
总结
本来就像简单的安利一下Blazor,回头看写了这么多,不会起到反作用吧,哈哈。不过话说回来,如果大家真的用了一段时间Blazor,你真的会发现这是一个宝藏框架,高性能,高安全性,生态也越来越丰富。
此外,Blazor作为微软推出的企业级的Web开发框架,除了Blazor Server模式,还有Blazor WASM和Blazor Auto模式,各有优劣,微软最新的云原生应用开发框架Aspire,进一步印证了Blazor在.NET生态系统中的重要地位。
总而言之吧,Blazor的出现,不仅为.NET开发者带来了全新的Web前端开发范式,更在历史的进程中证明了其独特的价值。它吸取了前代产品的经验教训,紧跟技术潮流,并与微软的云原生战略紧密结合。Blazor并非昙花一现,而是.NET生态系统在Web前端领域的一次重要布局,它将持续为.NET开发者提供高效、安全、现代化的Web应用开发体验。对于那些希望在.NET平台上构建未来Web应用的开发者来说,现在正是拥抱Blazor的最佳时机。
我知道很多年长一点的.NET开发者可能还是忘不了曾经Sliverlight的教训,关于这点,我也找到了一篇博客:claudiobernasconi.ch/blog/blazor...,可以参考。
总结起来还是我开头提到的,Blazor是一个成熟的框架,它极具个性,又兼容并包,而且社区生态发展繁荣,也得到了微软官方的大力支持和推广,是绝对值得深度投入学习和使用的。
好了,就这样啦。