简介:这份资源是面向 .NET 开发者与 Blazor 初学者的登录注册模块示例项目,基于 Blazor WebAssembly 与 C# 构建,通过伪造后端(模拟数据与本地存储)替代真实 API,帮助读者在服务端尚未就绪时也能完整跑通身份验证与授权流程。压缩包共 61 个文件,约 828KB,以 16 个 cs 源码、15 个 razor 组件、4 个 json 配置与 4 个 css 样式为主,另含 csproj、sln、wwwroot 静态资源及字体文件,结构清晰便于按模块阅读。项目覆盖 Blazor 组件化开发、表单验证、状态管理、路由导航、AuthenticationStateProvider 认证状态管理、HttpClient 模拟调用与 localStorage 持久化等关键知识点,并配有 FakeBackendHandler、AccountService、LocalStorageService 等可复用服务类,适合作为学习 Blazor 前后端交互与登录注册实现的练手参考。目前已有 500 人学习下载。
1. 伪造后端跑通 Blazor 登录注册:为什么我劝你先别急着接数据库
刚接触 Blazor 的人,十有八九会在登录注册这一步卡住。不是卡在写不出表单,而是卡在“后端还没好,我前端怎么联调”。等后端、等接口文档、等数据库建表,一周过去,页面还是静态的。我后来养成一个习惯:凡是认证这类交互密集的模块,先用一个伪造后端把整条链路跑通,再换成真接口。blazor-login-register 这个方向讲的正是这件事——用假数据源模拟登录和注册的完整流程,让 Blazor 的页面跳转、表单校验、状态保持、错误提示全部先动起来。
它解决的不是“如何做安全认证”,而是“如何在没有后端的情况下,把认证模块的前端骨架和交互逻辑验证到位”。适合两类人:一是刚学 Blazor、想找一个完整小模块练手的开发者;二是前后端并行开发时,前端不想被后端进度拖住的一线工程师。核心思路很简单:把 HTTP 调用换成一个本地服务,这个服务用内存字典存用户,返回和真实接口一样的结构。等你真正接后端时,只改注入实现,页面代码一行不动。
2. 伪造后端怎么选:内存字典、本地存储还是拦截器
2.1 三种伪造方式的适用边界
在 Blazor 里做伪造后端,常见做法有三种,选错了后面会反复返工。
第一种是内存字典。用一个Dictionary<string, User>存用户,注册就是写字典,登录就是查字典。优点是零依赖、启动快、代码量极小;缺点是刷新页面数据全丢,适合纯前端演示和单元测试。
第二种是浏览器本地存储。把用户列表序列化后写进 localStorage 或 sessionStorage。优点是刷新不丢,能模拟“已注册用户再次登录”的场景;缺点是存储容量有限,且多标签页之间数据不同步,容易在调试时产生“我明明注册了怎么登不上”的玄学问题。
第三种是 HTTP 拦截器。在 Blazor WebAssembly 里替换HttpClient的HttpMessageHandler,拦截发往后端的请求,直接返回伪造响应。优点是页面代码完全不用改,和真实后端调用方式一致;缺点是配置稍复杂,且只在 WebAssembly 模式下好用,Blazor Server 不适用。
我一般会这样选:如果只是验证页面跳转和表单逻辑,用内存字典;如果要演示“注册后关掉浏览器再回来还能登录”,用本地存储;如果目标是“将来无缝切换真后端”,用拦截器。下面这张表可以帮你快速决策。
| 方式 | 数据持久性 | 代码改动量 | 适用模式 | 切换真后端成本 |
|---|---|---|---|---|
| 内存字典 | 刷新即丢 | 最小 | Server / WASM | 中,需替换服务实现 |
| 本地存储 | 刷新保留 | 小 | WASM 为主 | 中,需清理存储逻辑 |
| HTTP 拦截器 | 刷新即丢 | 中 | WASM | 低,移除拦截器即可 |
2.2 用内存字典搭一个最小伪造认证服务
先定义一个用户模型和认证服务接口,这一步和真实项目保持一致,将来换实现时接口不变。
// Models/User.cs public class User { public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; // 仅用于伪造演示,真实项目绝不这样存 public string DisplayName { get; set; } = string.Empty; } // Services/IAuthService.cs public interface IAuthService { Task<AuthResult> LoginAsync(string username, string password); Task<AuthResult> RegisterAsync(string username, string password, string displayName); Task LogoutAsync(); bool IsAuthenticated { get; } string? CurrentUser { get; } } // Services/AuthResult.cs public class AuthResult { public bool Success { get; set; } public string? ErrorMessage { get; set; } }接口里IsAuthenticated和CurrentUser是为了让页面能根据登录状态切换显示内容。AuthResult用统一结构返回成功或失败,页面只需要判断Success字段,不用关心底层是字典还是 HTTP。
接下来是内存字典实现。注意这里加了简单的用户名重复校验和空值校验,这些校验逻辑将来可以直接搬到真实后端。
// Services/FakeAuthService.cs public class FakeAuthService : IAuthService { // 用静态字典模拟数据库,进程内共享 private static readonly Dictionary<string, User> _users = new(StringComparer.OrdinalIgnoreCase); private string? _currentUser; public bool IsAuthenticated => _currentUser != null; public string? CurrentUser => _currentUser; public Task<AuthResult> RegisterAsync(string username, string password, string displayName) { if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) return Task.FromResult(new AuthResult { Success = false, ErrorMessage = "用户名和密码不能为空" }); if (_users.ContainsKey(username)) return Task.FromResult(new AuthResult { Success = false, ErrorMessage = "该用户名已被注册" }); _users[username] = new User { Username = username, Password = password, DisplayName = displayName }; return Task.FromResult(new AuthResult { Success = true }); } public Task<AuthResult> LoginAsync(string username, string password) { if (!_users.TryGetValue(username, out var user)) return Task.FromResult(new AuthResult { Success = false, ErrorMessage = "用户不存在" }); if (user.Password != password) return Task.FromResult(new AuthResult { Success = false, ErrorMessage = "密码错误" }); _currentUser = username; return Task.FromResult(new AuthResult { Success = true }); } public Task LogoutAsync() { _currentUser = null; return Task.CompletedTask; } }_users用static是为了在多个组件之间共享数据,否则每次注入新实例字典都是空的。StringComparer.OrdinalIgnoreCase让用户名不区分大小写,避免用户注册Alice后用alice登录失败这种低级问题。所有方法返回Task是为了和真实异步接口签名一致,将来换成HttpClient调用时页面代码不用改。
2.3 在 Program.cs 里注册服务并接入页面
服务写好后,在Program.cs里注册。Blazor Server 和 WebAssembly 的注册方式略有不同,但核心都是把接口映射到伪造实现。
// Program.cs(Blazor Server 示例) builder.Services.AddScoped<IAuthService, FakeAuthService>();用Scoped而不是Singleton,是因为真实场景中认证状态通常和用户会话绑定。虽然这里用静态字典绕过了实例隔离,但保持Scoped能让将来切换实现时不用改注册代码。
页面部分,登录组件只需要注入IAuthService,在提交时调用LoginAsync,根据返回值决定跳转还是显示错误。
@* Pages/Login.razor *@ @page "/login" @inject IAuthService AuthService @inject NavigationManager Nav <h3>登录</h3> @if (!string.IsNullOrEmpty(_error)) { <p style="color:red">@_error</p> } <input @bind="_username" placeholder="用户名" /> <input @bind="_password" type="password" placeholder="密码" /> <button @onclick="HandleLogin">登录</button> @code { private string _username = string.Empty; private string _password = string.Empty; private string? _error; private async Task HandleLogin() { var result = await AuthService.LoginAsync(_username, _password); if (result.Success) { Nav.NavigateTo("/"); } else { _error = result.ErrorMessage; } } }@bind负责双向绑定输入框,@onclick触发登录逻辑。登录成功后用NavigationManager跳转到首页,失败则把错误信息赋给_error并显示。这里没有用EditForm,是为了让代码更短、更容易看清核心流程。真实项目里建议换成EditForm加数据注解,能自动处理校验和提交状态。
3. 注册、登录、状态保持:把三个页面串成完整闭环
3.1 注册页的重复校验与密码确认
注册页比登录页多两个点:密码确认和用户名重复提示。密码确认是纯前端校验,用户名重复需要调用伪造服务。
@* Pages/Register.razor *@ @page "/register" @inject IAuthService AuthService @inject NavigationManager Nav <h3>注册</h3> @if (!string.IsNullOrEmpty(_error)) { <p style="color:red">@_error</p> } <input @bind="_username" placeholder="用户名" /> <input @bind="_password" type="password" placeholder="密码" /> <input @bind="_confirm" type="password" placeholder="确认密码" /> <button @onclick="HandleRegister">注册</button> @code { private string _username = string.Empty; private string _password = string.Empty; private string _confirm = string.Empty; private string? _error; private async Task HandleRegister() { if (_password != _confirm) { _error = "两次输入的密码不一致"; return; } var result = await AuthService.RegisterAsync(_username, _password, _username); if (result.Success) { Nav.NavigateTo("/login"); } else { _error = result.ErrorMessage; } } }密码确认放在调用服务之前判断,避免无意义的服务调用。注册成功后跳转到登录页,而不是直接登录,这样能顺便验证“注册的用户确实能登录”这条链路。DisplayName这里直接用了用户名,真实项目可以加一个昵称输入框。
3.2 用 CascadingAuthenticationState 保持登录状态
页面之间跳转时,导航栏需要知道当前是否已登录,才能显示“登录/注册”还是“退出”。Blazor 自带CascadingAuthenticationState组件,配合AuthorizeView可以很干净地实现。
@* App.razor 或 MainLayout.razor 中包裹 *@ <CascadingAuthenticationState> <Router AppAssembly="@typeof(App).Assembly"> @* 路由配置 *@ </Router> </CascadingAuthenticationState>然后在导航栏里用AuthorizeView区分登录状态。
@* Shared/NavMenu.razor *@ <AuthorizeView> <Authorized> <span>你好,@context.User.Identity?.Name</span> <button @onclick="HandleLogout">退出</button> </Authorized> <NotAuthorized> <a href="/login">登录</a> <a href="/register">注册</a> </NotAuthorized> </AuthorizeView> @code { [Inject] private IAuthService AuthService { get; set; } = default!; [Inject] private NavigationManager Nav { get; set; } = default!; private async Task HandleLogout() { await AuthService.LogoutAsync(); Nav.NavigateTo("/login", forceLoad: true); } }这里有个关键点:CascadingAuthenticationState默认依赖AuthenticationStateProvider,而我们的伪造服务并没有实现这个提供程序。所以上面这段代码要真正工作,需要额外写一个自定义AuthenticationStateProvider,把FakeAuthService的登录状态包装成AuthenticationState。这是伪造后端方案里最容易翻车的地方——很多人以为包一层CascadingAuthenticationState就完事了,结果AuthorizeView永远显示未登录。
3.3 自定义 AuthenticationStateProvider 打通状态通知
正确的做法是继承AuthenticationStateProvider,在登录和退出时主动通知状态变化。
// Services/FakeAuthStateProvider.cs public class FakeAuthStateProvider : AuthenticationStateProvider { private readonly IAuthService _authService; public FakeAuthStateProvider(IAuthService authService) { _authService = authService; } public override Task<AuthenticationState> GetAuthenticationStateAsync() { if (_authService.IsAuthenticated && _authService.CurrentUser != null) { var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, _authService.CurrentUser) }, "FakeAuth"); var user = new ClaimsPrincipal(identity); return Task.FromResult(new AuthenticationState(user)); } return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); } // 登录或退出后调用,通知 Blazor 重新获取状态 public void NotifyStateChanged() { NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }注册时要把IAuthService和AuthenticationStateProvider都指向同一个伪造实例,否则状态不同步。
// Program.cs 中补充注册 builder.Services.AddScoped<FakeAuthService>(); builder.Services.AddScoped<IAuthService>(sp => sp.GetRequiredService<FakeAuthService>()); builder.Services.AddScoped<AuthenticationStateProvider>(sp => new FakeAuthStateProvider(sp.GetRequiredService<IAuthService>()));然后在登录和退出成功后调用NotifyStateChanged。这一步不做,导航栏就不会刷新,用户会以为登录没生效。我当初就是漏了这行通知,盯着页面刷新了十几次才反应过来。
4. 避坑与排查:伪造后端最容易翻车的五个地方
4.1 刷新后登录状态丢失,页面跳回登录页
现象:登录成功后跳转到首页,手动刷新浏览器,导航栏又变成未登录状态。
原因:内存字典和_currentUser都存在进程内存里,Blazor WebAssembly 刷新等于重新加载整个应用,内存全部清空。Blazor Server 虽然连接断开后重连,但Scoped服务实例也会重建。
解决:如果演示需要刷新保留,把_currentUser和用户字典写入sessionStorage,在GetAuthenticationStateAsync里先从存储读取。或者接受这个限制,明确它只用于开发阶段联调。
4.2 注册成功但登录提示“用户不存在”
现象:注册页提示成功,跳转到登录页输入同样的用户名密码,却提示用户不存在。
原因:FakeAuthService注册时用了AddScoped,但页面组件每次导航都创建新实例,静态字典虽然共享,但如果注册和登录分别注入了不同的服务实例,且字典不是静态的,就会各存各的。
解决:确保用户字典是static,或者把服务注册为Singleton。我一般用静态字典加Scoped注册,兼顾共享和将来替换。
4.3 AuthorizeView 始终显示未授权
现象:明明已经登录,AuthorizeView的Authorized分支就是不显示。
原因:只加了CascadingAuthenticationState,但没有注册自定义AuthenticationStateProvider,Blazor 用的是默认提供程序,永远返回未认证。
解决:按 3.3 节实现并注册FakeAuthStateProvider,登录成功后调用NotifyStateChanged。检查Program.cs里AuthenticationStateProvider的注册是否在AddAuthorizationCore之后。
4.4 密码明文存储被代码扫描工具报警
现象:提交代码时静态扫描工具报“硬编码密码”或“明文密码存储”。
原因:伪造服务里User.Password直接存了明文,虽然是演示,但扫描工具不区分场景。
解决:在伪造层做一次简单哈希,比如用SHA256加固定盐,让代码看起来不像明文。同时在注释里写明“仅用于伪造演示,真实项目必须用服务端哈希加盐”。
4.5 切换到真实后端时页面报错“无法解析 IAuthService”
现象:把FakeAuthService删掉换成HttpAuthService后,页面注入失败。
原因:新服务没有注册,或者注册的生命周期和页面不匹配。
解决:在Program.cs里把IAuthService的映射改到新实现,保持接口不变。如果新实现依赖HttpClient,记得注册HttpClient并注入。页面代码不需要任何改动,这正是接口隔离的价值。
5. 从伪造到真实:一个替换技巧和我的验证习惯
伪造后端最大的价值不是“假”,而是“可替换”。我通常会在FakeAuthService里留一个开关,通过配置决定用假数据还是真接口,这样同一套页面代码可以在两种模式下来回切。
// Program.cs 中根据配置选择实现 var useFakeAuth = builder.Configuration.GetValue<bool>("UseFakeAuth"); if (useFakeAuth) { builder.Services.AddScoped<FakeAuthService>(); builder.Services.AddScoped<IAuthService>(sp => sp.GetRequiredService<FakeAuthService>()); builder.Services.AddScoped<AuthenticationStateProvider>(sp => new FakeAuthStateProvider(sp.GetRequiredService<IAuthService>())); } else { builder.Services.AddScoped<IAuthService, HttpAuthService>(); builder.Services.AddScoped<AuthenticationStateProvider, RealAuthStateProvider>(); }appsettings.Development.json里设"UseFakeAuth": true,生产配置里设false。这样本地开发不依赖后端,部署时自动切到真实接口。HttpAuthService的实现和FakeAuthService保持同样的接口签名,内部用HttpClient发请求,解析同样的AuthResult结构。
验证方面,我有一个习惯:每加一个伪造方法,先写一个最小页面调用它,确认返回值符合预期,再接到正式页面上。比如注册方法写完后,我会临时加一个/debug/register页面,点一下按钮看字典里有没有数据。这个习惯帮我省掉了很多“页面报错但不知道是服务问题还是绑定问题”的排查时间。
还有一个技巧:在FakeAuthService里加一个GetAllUsers()方法,只在开发环境暴露,用来确认注册数据到底写进去没有。这个方法不要出现在IAuthService接口里,而是放在具体类上,页面通过@inject FakeAuthService按需调用。这样接口保持干净,调试时又有后门可走。
最后说一个我踩过的坑:伪造后端的错误提示不要写得太模糊。早期我所有失败都返回“操作失败”,结果分不清是用户名重复还是密码错误,调试时只能打断点。后来改成具体错误信息,页面直接显示,一眼就能定位。这个习惯延续到了真实项目里——后端返回的错误码再细,前端也要映射成用户能看懂的话。
希望帮到你。
本文还有配套的精品资源,点击获取