简介:基于Blazor WebAssembly实现的登录与注册模块,面向C#/.NET开发者,解决无需JavaScript库即可完成前端交互与身份验证的问题,采用伪造后端模拟API调用以降低开发门槛。压缩包共61个文件,以.cs源码、.razor组件、.json配置和.css样式为主,整体仅828KB,目录按Services、Models、Pages、Shared等模块划分,便于逐段阅读。目前已有500人学习下载。示例涵盖Blazor组件化开发、数据注解式表单验证、路由导航与NavigationManager跳转,并通过FakeBackendHandler拦截HttpClient请求,配合localStorage保存用户会话,同时演示AuthenticationStateProvider管理认证状态。对于希望快速上手Blazor WebAssembly组件复用、客户端状态管理和前后端交互模拟的初学者,是一份结构清晰、可直接运行的参考实现。
1. 为什么要在 Blazor 里先搭一套伪造后端登录注册
后端接口还在设计阶段,前端页面却已经画好了登录和注册弹窗,这种情况在 C# 项目里太常见了。等接口联调会拖慢进度,硬编一个固定返回又无法覆盖“用户已存在”“密码错误”“Token 过期”这些真实分支。blazor-login-register 的思路是把伪造后端作为项目的一等公民:用内存或 JSON 文件存储用户,在 Blazor 应用内实现一套完整的登录、注册、Token 校验流程。这套方案的直接价值是让前端认证模块先闭环,业务逻辑、表单校验、状态管理都能独立验证。适合正在做 Blazor WebAssembly 或 Blazor Server 认证模块的 C# 开发者,尤其是后端进度还不明确、又不想干等接口的团队。
2. 伪造后端从哪下手:先定接口契约,再选数据载体
2.1 接口契约:登录、注册、校验三个端点必须先写清楚
伪造后端不能只在组件里写两个 if 就完事,那样登录注册逻辑绑死在 UI 层,后面替换真实 API 几乎要重写。我一般会先写一份接口约定,用 C# 的 record 类型把请求和响应模型固定下来,让前端和后端都能对着同一份数据结构开发。
// Models/AuthModels.cs public record LoginRequest(string Username, string Password); public record RegisterRequest(string Username, string Password, string Email); public record AuthResponse(bool Success, string Token, string Message, string Username); // 伪造后端校验 Token 用的请求/响应 public record TokenValidationRequest(string Token); public record TokenValidationResponse(bool IsValid, string Username);这里把 LoginRequest 和 RegisterRequest 定义成纯数据载体,是因为登录注册字段在前后端之间传播时,不应该夹杂任何 UI 特性。用 record 而不是 class,除了语法更简洁,还能免费获得值比较能力,后面写单元测试断言时可以直接比较两个对象字段。
参数说明:Username 和 Password 是必须字段,Email 在注册请求里可空但建议保留。AuthResponse 里的 Message 字段承载错误码,比如user_not_found、bad_password、username_taken,前端拿 Error Code 做映射,而不是直接展示英文字符串。TokenValidationRequest 设计成独立模型,是为了让前端的“每次请求都带 Token”习惯从第一天就养成,避免后续换成真实 API 时手忙脚乱。
定义好模型后,伪造后端必须把所有路由集中到一个服务类里,不能散落在各个 razor 页面中。下面给出核心方法的签名:
// Services/MockAuthService.cs public class MockAuthService { public Task<AuthResponse> LoginAsync(LoginRequest request); public Task<AuthResponse> RegisterAsync(RegisterRequest request); public Task<TokenValidationResponse> ValidateTokenAsync(string token); public Task<bool> LogoutAsync(string token); }每个方法返回 Task 而不是同步结果,这是刻意模仿真实 HTTP 调用的异步模式。Blazor 组件里统一用await调用,将来替换成 HttpClient 时,不用改调用点。如果伪造后端返回同步结果,组件代码里就不需要 async/await,等接真实 API 时要改一堆地方,这就是给自己挖坑。
2.2 数据载体:用内存字典还是 JSON 文件,区别在重启后的状态
伪造后端存储用户有两条路线:存在静态 Dictionary 里,实现最快,但 Blazor 应用重新编译或刷新进程后数据全部丢失;写进本地 JSON 文件,数据持久化在磁盘,适合反复调试注册功能时不用每次重新创建账号。这个项目建议选 JSON 文件,因为登录注册的测试场景天然跨页面跳转,需要重启服务调试 UI 样式,如果每次重启都要从头注册,效率太低。
// Services/UserStore.cs public class UserStore { private readonly string _storagePath; private Dictionary<string, UserRecord> _users; public UserStore(IWebHostEnvironment env) { _storagePath = Path.Combine(env.ContentRootPath, "App_Data", "users.json"); _users = LoadUsers(); } private Dictionary<string, UserRecord> LoadUsers() { if (!File.Exists(_storagePath)) { return new Dictionary<string, UserRecord>( StringComparer.OrdinalIgnoreCase); } var json = File.ReadAllText(_storagePath); return JsonSerializer.Deserialize<Dictionary<string, UserRecord>>(json) ?? new Dictionary<string, UserRecord>( StringComparer.OrdinalIgnoreCase); } public void SaveUsers() { var dir = Path.GetDirectoryName(_storagePath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var json = JsonSerializer.Serialize(_users, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(_storagePath, json); } }这个 UserRecord 类要单独定义,字段应该明确区分存储字段和业务字段:
public class UserRecord { public string Username { get; set; } public string PasswordHash { get; set; } public string Email { get; set; } public DateTime CreatedAt { get; set; } }注意 LoadUsers 里给 Dictionary 用了StringComparer.OrdinalIgnoreCase,这样“Tom”和“tom”会被认为同一个用户,符合大多数真实系统的业务规则。如果漏掉这个比较器,伪造后端的行为和真实数据库的 unique 索引不一致,等接真实 API 时才发现大小写用户名重复注册,血泪代价很大。
SaveUsers 方法在每次注册成功或修改密码后调用,不要放在构造里。JSON 文件路径中的 App_Data 目录可以手动创建,也可以用Directory.CreateDirectory兜底。文件格式用缩进模式,方便开发时直接打开查看注册了哪些用户。
2.3 Token 的伪造逻辑:用 GUID 加时间戳模拟 JWT,别真的去搞密钥
真实 JWT 要用密钥签名、配置发行人、受众,这套东西放在伪造后端里会干扰精力。伪造 Token 的目的只是让前端有东西可存、可传、可校验,所以用一个带过期时间的 Base64 字符串就够了。
public string GenerateToken(string username) { var quote = $"{Guid.NewGuid():N}|{DateTime.UtcNow.AddHours(2):O}|{username}"; return Convert.ToBase64String(Encoding.UTF8.GetBytes(quote)); } public bool TryValidateToken(string token, out string username) { username = null; if (string.IsNullOrEmpty(token)) return false; try { var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(token)); var parts = decoded.Split('|'); if (parts.Length != 3) return false; var expireTime = DateTime.Parse(parts[1], null, DateTimeStyles.RoundtripKind); if (expireTime < DateTime.UtcNow) return false; username = parts[2]; return true; } catch { return false; } }GenerateToken 里把 GUID、过期时间、用户名用竖线分隔拼接后 Base64 编码。GUID 保证每次生成的 Token 都不同,防止前端把固定 Token 硬编码。过期时间用 ISO 8601 格式是为了统一解析,避免不同环境下的日期格式歧义。
TryValidateToken 里拆解字符串后,先检查是否过期,再取出用户名。这里刻意没有验证签名或篡改,因为伪造后端不需要防御攻击,只需要模拟“Token 无效”的返回路径。前端如果手动改乱了 Token,解析时会走 catch 分支返回 false,这在验证前端跳转逻辑时完全够用。
参数说明:过期时间默认 2 小时,你可以改成AddMinutes(30)来测试前端自动登出的计时逻辑。如果你需要模拟 refresh token 刷新机制,可以再生成一个 refreshToken 字段,但基本版里一个 Token 字段就够,加多了反而让前端误以为要去实现刷新流程。
2.4 把 Mock 服务挂到应用里:延迟与异常分支不能少
伪造后端还需要模拟网络延迟和异常抛出,否则前端永远不知道真实接口慢起来是什么体验。注册和登录方法里,我一般让响应延迟 300ms 到 800ms,随机取值,这样 Loading 状态就能真实展示。
public async Task<AuthResponse> LoginAsync(LoginRequest request) { await Task.Delay(Random.Shared.Next(300, 800)); if (!_users.TryGetValue(request.Username, out var user)) { return new AuthResponse(false, null, "user_not_found", null); } if (user.PasswordHash != HashPassword(request.Password)) { return new AuthResponse(false, null, "bad_password", null); } var token = GenerateToken(request.Username); return new AuthResponse(true, token, "ok", request.Username); }HashPassword 这里哪怕用最原始的 SHA256 也行,关键是让前端感觉到“密码不是纯文本存库”。模拟延迟的另一个作用是暴露前端的重复点击问题——如果组件没有 Lock 按钮,用户会在 800ms 里提交好几遍,导致重复注册。这个延迟就是故意用来逼你写 Loading 状态处理的。
路由注册方面,如果你把 Mock 服务以一个独立的 ASP.NET Core Minimal API 跑在另一个端口,那么注意在 Program.cs 里加 CORS,因为 Blazor WebAssembly 从浏览器调跨域 API 会被拦。但如果选择直接作为服务注入到 Blazor 应用内部,就不存在 CORS 问题,这也是伪造后端最常见的做法。
3. Blazor 登录注册组件的实现:表单校验与状态管理
3.1 注册组件:两次密码校验的两种做法
注册页面最常见的业务规则是“确认密码必须一致”,这个校验可以放在前端表单层,也可以放在伪造后端做二次校验。实际项目中前端用 DataAnnotations 解决第一层,后端再检查一遍是为了兼容没有前端的客户端调用。伪造后端阶段只需做前端校验。
@* Pages/Register.razor *@ @inject MockAuthService AuthService <EditForm Model="_model" OnValidSubmit="HandleSubmit"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="form-group"> <label>用户名</label> <InputText @bind-Value="_model.Username" class="form-control" /> <ValidationMessage For="() => _model.Username" /> </div> <div class="form-group"> <label>密码</label> <InputText @bind-Value="_model.Password" type="password" class="form-control" /> <ValidationMessage For="() => _model.Password" /> </div> <div class="form-group"> <label>确认密码</label> <InputText @bind-Value="_model.ConfirmPassword" type="password" class="form-control" /> <ValidationMessage For="() => _model.ConfirmPassword" /> </div> <button type="submit" class="btn btn-primary">注册</button> </EditForm>注意这里用的模型是一个单独的 RegisterModel,而不是直接使用 RegisterRequest。因为 ConfirmPassword 字段只属于前端 UI,不该进入后端契约。RegisterModel 里的验证特性是第一种做法:
public class RegisterModel { [Required(ErrorMessage = "用户名不能为空")] [MinLength(3, ErrorMessage = "用户名至少3位")] public string Username { get; set; } [Required(ErrorMessage = "密码不能为空")] [MinLength(6, ErrorMessage = "密码至少6位")] public string Password { get; set; } [Required(ErrorMessage = "确认密码不能为空")] [Compare(nameof(Password), ErrorMessage = "两次密码不一致")] public string ConfirmPassword { get; set; } }Compare是 DataAnnotations 自带特性,比较两个属性值,差异时直接显示错误。第二种做法是在 HandleSubmit 里手动判断_model.Password != _model.ConfirmPassword,然后追加到 validation messages。手动做法的好处是可以同时清空两个密码框,但代码量更多。我建议用 Compare 特性,因为 Blazor 的<ValidationMessage>会自动显示错误,少写不少逻辑。
HandleSubmit 里调用伪造后端后,还要处理业务错误:
private async Task HandleSubmit() { var request = new RegisterRequest( _model.Username, _model.Password, _model.Email); var result = await AuthService.RegisterAsync(request); if (result.Success) { Navigation.NavigateTo("/login"); } else { _errorMessage = result.Message switch { "username_taken" => "用户名已被占用", _ => "注册失败" }; } }这段映射里,username_taken是伪造后端返回的业务错误码,前端负责翻译成用户能看懂的文字。这里有一个容易忽略的点:Navigation.NavigateTo("/login")最好传一个绝对路径,不要用相对路径,否则在 Blazor 子路由下会拼出错误路径。
3.2 登录组件:Loading 状态与错误码映射
登录页面比注册页面更强调 Loading 状态,因为登录是高频操作,用户会在等待时反复点“登录”按钮,导致伪造后端收到多个并发请求。就算伪造后端没并发问题,真实 API 肯定会遇到。
@* Pages/Login.razor *@ <EditForm Model="_login" OnValidSubmit="LoginAsync"> <div class="form-group"> <label>用户名</label> <InputText @bind-Value="_login.Username" class="form-control" /> </div> <div class="form-group"> <label>密码</label> <InputText @bind-Value="_login.Password" type="password" class="form-control" /> </div> @if (!string.IsNullOrEmpty(_errorMessage)) { <div class="alert alert-danger">@_errorMessage</div> } <button type="submit" class="btn btn-primary" disabled="@_isSubmitting"> @(_isSubmitting ? "登录中..." : "登录") </button> </EditForm>后台逻辑用_isSubmitting字段控制按钮禁用,并且在 finally 里恢复。如果直接尝试用await Task.Delay模拟真实延迟,你会发现按钮在点击后没有立即变成 Loading,这是因为 EditForm 的 OnValidSubmit 在异步方法里的运行顺序问题,解决方案是先将_isSubmitting设为 true,然后立刻调用StateHasChanged()。
private async Task LoginAsync() { _isSubmitting = true; StateHasChanged(); try { var resp = await AuthService.LoginAsync( new LoginRequest(_login.Username, _login.Password)); if (resp.Success) { await _authStateProvider.SetTokenAsync(resp.Token, resp.Username); Navigation.NavigateTo("/", forceLoad: false); } else { _errorMessage = resp.Message switch { "user_not_found" => "用户名不存在", "bad_password" => "用户名或密码错误", _ => "登录暂时不可用" }; } } finally { _isSubmitting = false; StateHasChanged(); } }switch 表达式的作用是把后端 Error Code 映射成中文文案。这里的字符串映射是为了演示,如果你面临多语言需求,可以把它换成 Dictionary 或资源文件。映射逻辑要放在组件里而不是伪造后端,因为后端不应该关心前端用户的语言是中文还是英文。
3.3 认证状态管理:Scoped 服务加事件通知,避免组件各存各的
登录成功后,导航栏要变成用户信息加退出按钮,受保护页面要检查 Token 是否存在。如果每个组件各自维护一个 bool 值,状态很难同步。常见的架构是定义一个 AuthStateProvider 服务,作为认证状态的唯一数据源。
// Services/AuthStateProvider.cs public class AuthStateProvider { private readonly MockAuthService _authService; public string Token { get; private set; } public string Username { get; private set; } public bool IsAuthenticated => !string.IsNullOrEmpty(Token); public event Action StateChanged; public AuthStateProvider(MockAuthService authService) { _authService = authService; } public async Task SetTokenAsync(string token, string username) { Token = token; Username = username; StateChanged?.Invoke(); await Task.CompletedTask; } public void Logout() { Token = null; Username = null; StateChanged?.Invoke(); } }这个服务里StateChanged事件告诉所有订阅者“认证状态变了”,各组件在OnInitialized里订阅,并在事件发生时调用StateHasChanged()刷新 UI。在 Blazor Server 端,这个服务要注册成 Scoped 而不是 Singleton,否则所有用户共享同一个登录状态,那就非常危险。
为了简化订阅逻辑,可以写一个基类组件:
public class AuthComponentBase : ComponentBase, IDisposable { [Inject] protected AuthStateProvider AuthState { get; set; } protected override void OnInitialized() { AuthState.StateChanged += OnAuthStateChanged; } private void OnAuthStateChanged() => InvokeAsync(StateHasChanged); public void Dispose() { AuthState.StateChanged -= OnAuthStateChanged; } }其他页面比如 Profile.razor 继承 AuthComponentBase,然后在 UI 里判断AuthState.IsAuthenticated。这样做的好处是,登录和退出操作触发事件后,所有页面自动刷新,不需要手工写导航逻辑。
3.4 受保护页面的验证逻辑:伪造后端阶段就模拟真实管道
受保护页面不能只在 UI 上隐藏按钮,还要在进入页面时验证 Token 是否仍然有效。伪造后端阶段必须把这个逻辑做成一个可复用方法,否则将来替换真实 API 时,受保护页面的检查逻辑会被打回重做。
// Services/ProtectedPageHelper.cs public static class ProtectedPageHelper { public static async Task<NavigationResult> EnsureAuthenticatedAsync( AuthStateProvider authState, MockAuthService authService) { if (!authState.IsAuthenticated) { return NavigationResult.RedirectToLogin; } var validation = await authService.ValidateTokenAsync(authState.Token); if (!validation.IsValid) { authState.Logout(); return NavigationResult.RedirectToLogin; } return NavigationResult.Ok; } }在需要保护的组件里,先调用这个方法:
protected override async Task OnInitializedAsync() { var result = await ProtectedPageHelper.EnsureAuthenticatedAsync( AuthState, AuthService); if (result == NavigationResult.RedirectToLogin) { Navigation.NavigateTo("/login"); return; } // 继续加载页面数据 }注意这里调用了ValidateTokenAsync,意味着每次进入受保护页面都会调用一次伪造后端的方法。将来换成真实 API 时,这就是一次 HTTP 请求。如果你的 Blazor Server 项目对刷新频繁,可以改成从 localStorage 读取 Token 并做一次简单的有效期判断,但伪造后端阶段建议保持完整验证,这样迁移时只要替换ValidateTokenAsync的实现即可。
4. 避坑实践:伪造后端从入门到翻车的六个常见问题
4.1 刷新页面后登录状态丢失
现象:登录成功后跳转到首页一切正常,但按 F5 刷新页面,又跳回登录页。
原因:Blazor WebAssembly 应用进程在刷新时被完全销毁,内存里保存的 Token 和用户名全部清空。如果 AuthStateProvider 只是把 Token 存到属性里,没有持久化,刷新后自然找不到登录态。
解决:在 AuthStateProvider 初始化时从浏览器 localStorage 读取 Token。可以把 IJSInProcessRuntime 注入服务,或者用封装好的 Storage 帮助类。读取后调用ValidateTokenAsync验证是否过期,如果有效就恢复 Username。关键点是注册服务时使用 Scoped 生命周期,确保每个页面会话拿到的是同一个实例,但从 storage 恢复的逻辑只执行一次。
4.2 注册成功后跳转登录页,仍提示用户已存在
现象:注册成功后再去登录,输入同样的用户名和密码,却提示用户名不存在,或者反过来提示“用户不存在”。
原因:伪造后端的 UserStore 在启动时加载了 JSON 文件内容,注册时往内存字典里添加了新用户,但保存 JSON 文件的时机不对。如果注册回调里没有立即调用SaveUsers(),下次启动服务后用户数据丢失,自然就“不存在”了。
解决:注册方法里在成功添加用户后立刻调用SaveUsers()。还有另一个隐藏坑:如果注册方法里先检查用户名是否存在,再插入用户,但是对字典的读和写之间没有加锁,并发双击会导致两个请求都通过检查,然后抛异常。伪造后端阶段可以用lock (_lockObj)包住检查加写入的操作,模拟真实数据库的原子性。
4.3 HttpClient 跨域请求被浏览器拦截
现象:Blazor WebAssembly 项目里的登录按钮点击后,控制台报 CORS error,请求没有真正发出去。
原因:伪造后端如果作为独立 ASP.NET Core WebAPI 项目运行,而 Blazor WebAssembly 项目跑在另一个 localhost 端口,浏览器会拦截跨域 HTTP 响应。
解决:在伪造后端项目的 Program.cs 里显式添加 CORS 策略。不能用AllowAnyOrigin,因为真实项目中不可能放开所有来源,这里只允许本地开发地址。可以这样:
builder.Services.AddCors(options => { options.AddPolicy("DevCors", policy => policy.SetIsOriginAllowed(origin => origin.StartsWith("http://localhost:") || origin.StartsWith("https://localhost:"))); }); var app = builder.Build(); app.UseCors("DevCors");如果你选择把伪造后端直接注入到 Blazor Server 服务端,而不是走 HttpClient,那就没有 CORS 问题,但那样失去了模拟真实网络请求的意义,因为真实 API 一定是跨网络的。
4.4 手动改 users.json 文件后,程序读不到最新数据
现象:注册逻辑调试中,打开 users.json 看到用户已写入,但修改文件内容后重新登录,伪造后端用的还是旧数据。
原因:UserStore 构造函数里把 JSON 文件读到内存字典后,后续所有操作都以内存字典为准,文件只是启动时的一次性加载。手工改文件后没有触发内存刷新。
解决:有两种修复思路。一种是每次登录和注册都直接读文件,但这样效率低并且代码更繁琐。另一种是在 UserStore 内部维护一个文件监视器,或者提供 Refresh 方法,只在调试时调。最省事的做法是修改完文件后重启 Blazor 应用,伪造后端就能重新加载。如果不想重启,那就按下面的代码改成从文件恢复:
public Dictionary<string, UserRecord> GetUsers() { using var fs = new FileStream(_storagePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite); using var reader = new StreamReader(fs); var json = reader.ReadToEnd(); return JsonSerializer.Deserialize<Dictionary<string, UserRecord>>(json) ?? new Dictionary<string, UserRecord>(); }调用GetUsers()的时机放在每次校验密码之前,可以保证手动修改立即生效。这个方法在真实 API 场景下对应的是“每次查询数据库”,所以使用它会更贴近最终的交互模式。
4.5 密码框用密码管理器自动填充,导致校验值不对
现象:登录页面用浏览器自带密码管理器填充用户名和密码后,点登录提示“用户名或密码错误”,但手输就能成功。
原因:Blazor 的<InputText>默认绑定onchange事件,而密码管理器自动填充时不触发 onchange,导致模型里的 Password 字段还是空字符串或旧值。
解决:把密码输入框的绑定事件改成oninput,这样每次输入或填充都会更新模型:
<InputText @bind-Value="_login.Password" type="password" @bind:event="oninput" class="form-control" />这个小问题在伪造后端阶段就会出现,如果不处理,迁移到真实 API 时会变成特别玄学的 bug。注意注册页面的确认密码字段同样需要加@bind:event="oninput",否则自动填充也会导致两次密码不一致的假报错。
4.6 替换真实 API 后,受保护页面全部 401
现象:伪造后端阶段一切正常,把 MockAuthService 换成 HttpClient 调用真实 API 后,任何受保护页面请求都返回 401 Unauthorized。
原因:伪造后端的 Token 校验方法ValidateTokenAsync只在页面初始化时显式调用,并没有在每次 HTTP 请求时自动携带 Authorization Header。真实 API 需要前端在每次请求头里加 Token,如果伪造后端阶段没有在 HttpClient 的请求管道里统一添加,迁移后自然漏掉。
解决:在伪造后端阶段就定义一个自定义 DelegatingHandler,把 Token 从 AuthStateProvider 取出来加到请求头:
public class AuthMessageHandler : DelegatingHandler { private readonly AuthStateProvider _authState; public AuthMessageHandler(AuthStateProvider authState) { _authState = authState; } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (_authState.IsAuthenticated) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _authState.Token); } return await base.SendAsync(request, cancellationToken); } }注册 HttpClient 时加上这个 Handler:
builder.Services.AddScoped<AuthStateProvider>(); builder.Services.AddTransient<AuthMessageHandler>(); builder.Services.AddHttpClient("AuthApi") .AddHttpMessageHandler<AuthMessageHandler>();这样伪造后端阶段就模拟了“每个请求都自动携带 Token”的管道,等替换真实 API 时,这个 Handler 可以直接沿用,只需要改 BaseAddress 和认证地址。如果你在伪造后端阶段偷懒不做这层,迁移时就要在所有调用点手工加 Header,那几乎等同于重写。
5. 进阶:把伪造后端换成真实 API,迁移时只需改一个地方
先定义 IAuthService 接口,把 MockAuthService 和将来的 RealAuthService 都实现它。接口里就三个方法:LoginAsync、RegisterAsync、ValidateTokenAsync。组件里全部注入 IAuthService,而不是直接注入 MockAuthService。
public interface IAuthService { Task<AuthResponse> LoginAsync(LoginRequest request); Task<AuthResponse> RegisterAsync(RegisterRequest request); Task<TokenValidationResponse> ValidateTokenAsync(string token); }伪造后端的 MockAuthService 实现这个接口,里面保持现有逻辑。真实 API 的 RealAuthService 用 HttpClient 调用远程端点,返回的 AuthResponse 字段完全一致。在 Program.cs 里切换注册方式:
var useMock = builder.Configuration.GetValue<bool>("UseMockBackend"); if (useMock) { builder.Services.AddScoped<IAuthService, MockAuthService>(); builder.Services.AddScoped<UserStore>(); } else { builder.Services.AddScoped<IAuthService, RealAuthService>(); builder.Services.AddScoped<HttpClient>(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"]) }); }UseMockBackend这个配置项放在 appsettings.json 里,默认 true,部署到测试环境时改成 false。切换后,组件代码一行不用改,因为接口没变。真正需要改的是 Token 的生成和校验逻辑,但那是服务内部实现,与 UI 无关。
验证迁移是否成功的标准有三个。第一,登录成功后在 localStorage 里能看到 Token,且 Token 解密后包含用户名和过期时间;第二,直接访问一个受保护页面 URL,未登录状态会跳转登录,登录后能正常进入;第三,手动改坏 Token 后访问受保护页面,会被强制登出并回到登录页。这三条都通过,说明前端的认证闭环和真实 API 的认证闭环行为一致。
另外要关注 Token 刷新机制。伪造后端阶段 Token 过期时间是 2 小时,真实 API 可能只有 30 分钟,甚至带 refresh token。如果要支持刷新,可以在 AuthStateProvider 里加一个 RefreshToken 属性和 TryRefreshAsync 方法。伪造后端阶段可以直接返回一个新的 Token,真实 API 阶段则调用刷新端点。前端组件只需要在收到 401 响应时调用 TryRefreshAsync,然后重试一次请求。
从那以后,我每次搭 Blazor 项目,不管后端进度如何,都会先用伪造后端把认证管道完整实现一遍,尤其强制走一遍受保护页面携带 Authorization Header 的逻辑。这个习惯帮我避开了不少替换 API 时的翻车情况,希望帮到你。
本文还有配套的精品资源,点击获取