Blazor Server 前后端不分离项目简单身份验证
此基于Blazor Server 应用通过使用 SignalR 创建的实时连接运行。建立连接时,将处理基于 SignalR 的应用中的身份验证。身份验证可以基于 Cookie 或其他一些不记名令牌。Blazor Server 应用的安全性配置方式与 ASP.NET Core 应用相同。
Blazor Server 应用的内置 AuthenticationStateProvider 服务从 ASP.NET Core 的 HttpContext.User 获取身份验证状态数据。这就是身份验证状态与现有 ASP.NET Core 身份验证机制集成的方式
项目结构
- 解决方案
MyBlazorApp.sln
- Blazor Server项目
MyBlazorApp
(Blazor Server项目)Pages/
– 包含所有的页面组件(*.razor
)Components/
– 可复用的UI组件Authentication/
– 身份验证服务相关文件夹CustomAuthenticationStateProvider.cs/
– 自定义的认证状态提供器类UserAccount.cs/
– 用户DtoUserAccountService.cs/
– 用户服务UserSession.cs/
– 用户的会话信息
Shared/
– 前后端共享的组件或代码Data/
– 数据访问层和业务逻辑Models/
– 数据模型wwwroot/
– 静态文件,如CSS、JavaScript和图像Program.cs
– 配置Blazor Server应用程序的启动Startup.cs
– 配置服务和中间件(如果使用Startup类)
详细说明
- Pages: 包含Blazor应用的页面,每个页面都由一个
.razor
文件组成。这些页面负责用户界面的展示和用户交互。 - Components: 存放可复用的UI组件,组件可以在多个页面中使用,帮助减少代码重复。
- Shared: 用于存放前后端共享的代码,如共享的组件或帮助类。
- Data: 负责数据访问和业务逻辑。可以在这里实现与数据库的交互逻辑(如使用Entity Framework Core),以及任何必要的业务规则。
- Models: 定义应用程序使用的数据模型。这些模型通常用于表示数据库中的实体或其他数据结构。
- wwwroot: 包含所有的静态资源,如CSS样式表、JavaScript文件和图像文件。Blazor Server会将这些文件直接提供给浏览器。
- Program.cs: 定义应用程序的入口点,配置服务(如依赖注入)和应用程序生命周期。
- Startup.cs: 配置应用程序的请求管道和服务(如果使用Startup类)。包括中间件配置、路由设置等。
身份验证详解
Authentication/CustomAuthenticationStateProvider.cs
// 定义一个自定义的认证状态提供器类,继承自AuthenticationStateProvider
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
// 使用ProtectedSessionStorage来安全地存储用户会话信息
private readonly ProtectedSessionStorage _sessionStorage;
// 定义一个匿名用户的ClaimsPrincipal对象,表示未认证的用户
private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
// 构造函数,初始化会话存储对象
public CustomAuthenticationStateProvider(ProtectedSessionStorage sessionStorage)
{
_sessionStorage = sessionStorage;
}
// 重写基类的GetAuthenticationStateAsync方法,用于获取当前用户的认证状态
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
// 从会话存储中异步获取用户会话信息
var userSessionStorageResult = await _sessionStorage.GetAsync<UserSession>("UserSession");
// 检查获取结果是否成功,如果成功则获取用户会话信息,否则为null
var userSession = userSessionStorageResult.Success ? userSessionStorageResult.Value : null;
// 如果用户会话信息为null,返回匿名用户的认证状态
if (userSession == null)
return await Task.FromResult(new AuthenticationState(_anonymous));
// 如果用户会话信息存在,创建一个包含用户名和角色的ClaimsPrincipal对象
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, userSession.UserName), // 用户名声明
new Claim(ClaimTypes.Role, userSession.Role) // 角色声明
}, "CustomAuth")); // 使用自定义认证类型
// 返回包含用户信息的认证状态
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
}
catch
{
// 如果发生异常,返回匿名用户的认证状态
return await Task.FromResult(new AuthenticationState(_anonymous));
}
}
// 更新认证状态的方法,接收一个用户会话对象作为参数
public async Task UpdateAuthenticationState(UserSession userSession)
{
ClaimsPrincipal claimsPrincipal;
if (userSession != null)
{
// 如果用户会话信息不为null,将其存储到会话存储中
await _sessionStorage.SetAsync("UserSession", userSession);
// 创建包含用户名和角色的ClaimsPrincipal对象
claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, userSession.UserName), // 用户名声明
new Claim(ClaimTypes.Role, userSession.Role) // 角色声明
}));
}
else
{
// 如果用户会话信息为null,删除存储的会话信息
await _sessionStorage.DeleteAsync("UserSession");
// 设置为匿名用户
claimsPrincipal = _anonymous;
}
// 通知认证状态已更改,传递新的认证状态
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
}
}
Authentication
/UserAccount.cs
public class UserAccount
{
public string UserName { get; set; }
public string Password { get; set; }
public string Role { get; set; }
}
Authentication
/UserAccountService.cs
public class UserAccountService
{
private List<UserAccount> _users;
public UserAccountService()
{
_users = new List<UserAccount>
{
new UserAccount{ UserName = "admin", Password = "admin", Role = "Administrator" },
new UserAccount{ UserName = "user", Password = "user", Role = "User" }
};
}
public UserAccount? GetByUserName(string userName)
{
return _users.FirstOrDefault(x => x.UserName == userName);
}
}
Authentication
/UserSession.cs
public class UserSession
{
public string UserName { get; set; }
public string Role { get; set; }
}
Pages/Login.razor
@page "/login"
@using MyBlazorApp.Authentication
@inject UserAccountService userAccountService
@inject IJSRuntime js
@inject AuthenticationStateProvider authStateProvider
@inject NavigationManager navManager
<div class="row">
<div class="col-lg-4 offset-lg-4 pt-4 pb-4 border">
<div class="mb-3 text-center">
<h3>LOGIN</h3>
</div>
<div class="mb-3">
<label>User Name</label>
<input @bind="model.UserName" class="form-control" placeholder="User Name" />
</div>
<div class="mb-3">
<label>Password</label>
<input @bind="model.Password" type="password" class="form-control" placeholder="Password" />
</div>
<div class="mb-3 d-grid gap-2">
<button @onclick="Authenticate" class="btn btn-primary">Login</button>
</div>
</div>
</div>
@code {
private class Model
{
public string UserName { get; set; }
public string Password { get; set; }
}
private Model model = new Model();
private async Task Authenticate()
{
var userAccount = userAccountService.GetByUserName(model.UserName);
if (userAccount == null || userAccount.Password != model.Password)
{
await js.InvokeVoidAsync("alert", "Invalid User Name or Password");
return;
}
var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
await customAuthStateProvider.UpdateAuthenticationState(new UserSession
{
UserName = userAccount.UserName,
Role = userAccount.Role
});
navManager.NavigateTo("/", true);
}
}
Shared/RedirectToLogin
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation
<p>用户未授权,正在重定向到登录页面...</p>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity == null || !user.Identity.IsAuthenticated)
{
// 用户未认证,重定向到登录页面
Navigation.NavigateTo("/login", true);
}
}
}
}
Shared/MainLayout.razor
@using MyBlazorApp.Authentication
@inherits LayoutComponentBase
@inject AuthenticationStateProvider authStateProvider
@inject NavigationManager navManager
<PageTitle>MyBlazorApp</PageTitle>
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
<AuthorizeView>
<Authorized>
<a @onclick="Logout" href="javascript:void(0)">Logout</a>
</Authorized>
<NotAuthorized>
<a href="/login">Login</a>
</NotAuthorized>
</AuthorizeView>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
@code{
private async Task Logout()
{
var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
await customAuthStateProvider.UpdateAuthenticationState(null);
navManager.NavigateTo("/", true);
}
}
App.razor
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
<Authorizing>
You are getting authorized
</Authorizing>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthenticationCore();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<ProtectedSessionStorage>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddSingleton<UserAccountService>();
builder.Services.AddSingleton<WeatherForecastService>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
使用方式
在页面中使用
@page "/counter"
@attribute [Authorize(Roles = "Administrator,User")]
在组件中使用
<AuthorizeView Roles="Administrator,User">
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
</Authorized>
</AuthorizeView>
<AuthorizeView Roles="Administrator">
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</Authorized>
</AuthorizeView>