启用自动渲染与预渲染的Blazor默认模板无法从客户端调用控制器
Blazor预渲染模式下登录功能问题及解决方案
问题描述
使用全局启用自动渲染和预渲染的新版Blazor默认模板开发,Login.Razor组件位于客户端项目中。目标是输入用户名和密码点击登录按钮时,通过HttpClient向同解决方案下服务器项目的AuthController>login端点发起Post请求,但实际遇到HttpClient基地址无法配置、请求无法发起的问题。
已知服务器首次渲染组件后会将文件下载到浏览器,后续页面加载时组件可在浏览器渲染。需要为登录功能配置客户端与服务端服务:客户端服务调用API,服务端服务实现登录逻辑(绕过控制器),但不清楚具体实现方式,同时希望将token存储到localStorage中。
当前代码
客户端 Program.cs(原Startup.cs)
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddBlazoredLocalStorage(); builder.Services.AddScoped<IBaseHttpService, BaseHttpService>(); await builder.Build().RunAsync();
客户端 Login.razor
@inject IJSRuntime JS @inject IBaseHttpService HttpService @code { private LoginModel loginModel = new LoginModel(); private async Task HandleLogin() { var response = await HttpService.Post(loginModel, "api/auth/login"); if (response.Success) { var result = response.Data; await JS.InvokeVoidAsync("localStorage.setItem", "authToken", result); // Redirect or perform other actions after successful login } else { // Handle login failure } } public class LoginResult { public string Token { get; set; } } }
客户端 BaseHttpService
public class BaseHttpService : IBaseHttpService { public readonly HttpClient client; public readonly ILocalStorageService localStorage; public BaseHttpService(HttpClient client, ILocalStorageService localStorage) { this.client = client; this.localStorage = localStorage; } public async Task<Response<T>> Post<T>(T model, string endPoint) { Response<T> response; try { var user = System.Text.Json.JsonSerializer.Serialize(model); var requestContent = new StringContent(user, Encoding.UTF8, "application/json"); var responseMessage = await client.PostAsync(endPoint, requestContent); if (responseMessage.IsSuccessStatusCode) { var jsonString = responseMessage.Content.ReadAsStringAsync().Result; var myObject = JsonConvert.DeserializeObject<T>(jsonString); response = new Response<T> { Data = myObject!, Success = true, }; return response; } else { return new Response<T> { Data = (T)Activator.CreateInstance(typeof(T))!, Message = responseMessage.ReasonPhrase ?? string.Empty, Success = false }; } } catch (Exception) { throw; } return response; } }
服务器 Program.cs(缩写,原Startup.cs)
... builder.Services.AddScoped<IBaseHttpService, BaseHttpService>(); ... // Add CORS services builder.Services.AddCors(options => { options.AddPolicy("AllowAll", builder => { _ = builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); ... // Use CORS policy app.UseCors("AllowAll");
服务器 AuthController
[Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; private readonly IConfiguration _configuration; public AuthController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IConfiguration configuration) { _userManager = userManager; _signInManager = signInManager; _configuration = configuration; } [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginModel model) { var user = await _userManager.FindByNameAsync(model.Username); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { var token = GenerateJwtToken(user); return Ok(new { token }); } return Unauthorized(); } private string GenerateJwtToken(ApplicationUser user) { var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } }
解决方案
1. 修复HttpClient基地址配置问题
预渲染模式下,客户端运行时需要指向服务器API的地址,而非客户端自身的BaseAddress。修改客户端Program.cs中的HttpClient注册:
// 替换原HttpClient注册代码,直接指定服务器API地址,或通过配置获取 builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:5001/") // 替换为你的服务器实际地址 });
如果希望通过配置动态获取,可在客户端appsettings.json中添加API地址配置:
{ "ApiBaseUrl": "https://localhost:5001/" }
然后在Program.cs中读取:
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress; builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) });
2. 绕过控制器实现登录逻辑(服务端直接调用)
创建登录服务接口,在服务器端实现登录逻辑,通过Blazor服务注入在组件中调用,避免HTTP请求:
步骤1:定义登录服务接口(放在共享项目中)
public interface IAuthService { Task<LoginResult> LoginAsync(LoginModel model); } public class LoginModel { public string Username { get; set; } public string Password { get; set; } } public class LoginResult { public string Token { get; set; } public bool Success { get; set; } public string Message { get; set; } }
步骤2:在服务器端实现服务
public class AuthService : IAuthService { private readonly UserManager<ApplicationUser> _userManager; private readonly IConfiguration _configuration; public AuthService(UserManager<ApplicationUser> userManager, IConfiguration configuration) { _userManager = userManager; _configuration = configuration; } public async Task<LoginResult> LoginAsync(LoginModel model) { var user = await _userManager.FindByNameAsync(model.Username); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { var token = GenerateJwtToken(user); return new LoginResult { Token = token, Success = true }; } return new LoginResult { Success = false, Message = "用户名或密码错误" }; } private string GenerateJwtToken(ApplicationUser user) { // 复用原控制器中的token生成逻辑 var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } }
步骤3:在服务器Program.cs中注册服务
builder.Services.AddScoped<IAuthService, AuthService>();
步骤4:修改Login.razor组件调用服务
预渲染时服务器端无法访问localStorage,需确保客户端相关操作仅在浏览器环境执行:
@inject IJSRuntime JS @inject IAuthService AuthService @inject NavigationManager NavigationManager @inject IWebAssemblyJSRuntime WebAssemblyJSRuntime @code { private LoginModel loginModel = new LoginModel(); private bool isClientSide = false; protected override void OnInitialized() { // 判断是否在客户端运行 isClientSide = WebAssemblyJSRuntime != null; } private async Task HandleLogin() { var result = await AuthService.LoginAsync(loginModel); if (result.Success) { // 仅在客户端时存储token到localStorage if (isClientSide) { await JS.InvokeVoidAsync("localStorage.setItem", "authToken", result.Token); } // 跳转到主页或其他页面 NavigationManager.NavigateTo("/"); } else { // 处理登录失败逻辑,比如显示错误信息 } } }
3. 预渲染模式下的注意事项
- 预渲染阶段服务器端无法访问浏览器API(如localStorage),必须确保相关操作仅在客户端执行。
- 如果仍需使用HttpClient调用API,要确保预渲染时跳过HTTP请求,或者配置正确的基地址,避免服务器端发起无效请求。
内容的提问来源于stack exchange,提问作者David.Warwick
相关产品推荐
相关产品推荐

