You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

启用自动渲染与预渲染的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:37:33