Blazor WebAssembly独立应用访问已认证ASP.NET Core Web API问题
解决方案:Blazor WASM独立应用对接ASP.NET Core API认证
核心问题分析
你的场景中,Blazor WASM独立应用(跨端口)无法复用API的认证状态,本质是独立模式下Blazor没有内置的ASP.NET Core身份认证集成,需要手动处理令牌的存储、附加到后续请求头,或者配置跨域Cookie共享。以下是两种最直接的实现方案,优先推荐JWT令牌方式(跨域更简单)。
方案1:JWT令牌认证(推荐)
假设你的API /login 接口返回JWT格式的访问令牌,按以下步骤实现:
1. 客户端创建认证服务
负责登录、令牌存储与读取:
public class AuthService { private readonly HttpClient _httpClient; private readonly IJSRuntime _jsRuntime; public AuthService(HttpClient httpClient, IJSRuntime jsRuntime) { _httpClient = httpClient; _jsRuntime = jsRuntime; } // 登录请求,存储令牌到LocalStorage public async Task<bool> Login(string email, string password) { var loginPayload = new { Email = email, Password = password }; var response = await _httpClient.PostAsJsonAsync("/login", loginPayload); if (!response.IsSuccessStatusCode) return false; var tokenResult = await response.Content.ReadFromJsonAsync<LoginResponse>(); await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "authToken", tokenResult.AccessToken); return true; } // 退出登录,清除令牌 public async Task Logout() { await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "authToken"); } // 获取存储的令牌 public async Task<string?> GetToken() { return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "authToken"); } } // 对应API返回的令牌结构 public class LoginResponse { public string AccessToken { get; set; } = string.Empty; }
2. 自定义请求消息处理器
自动将令牌附加到所有API请求的Authorization头:
public class AuthMessageHandler : DelegatingHandler { private readonly IJSRuntime _jsRuntime; public AuthMessageHandler(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", "authToken"); if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); } }
3. 配置客户端Program.cs
注册服务并配置带认证的HttpClient:
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<HeadOutlet>("head::after"); // 注册认证服务 builder.Services.AddScoped<AuthService>(); // 注册自定义消息处理器 builder.Services.AddScoped<AuthMessageHandler>(); // 配置API客户端,自动附加令牌 builder.Services.AddHttpClient("ApiClient", client => { client.BaseAddress = new Uri("https://localhost:5001/"); // 替换为你的API地址 }) .AddHttpMessageHandler<AuthMessageHandler>(); // 注册便捷使用的HttpClient实例 builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient")); await builder.Build().RunAsync();
4. 登录与数据组件示例
登录组件(Login.razor)
@page "/login" @inject AuthService AuthService @inject NavigationManager NavManager <EditForm Model="@loginModel" OnValidSubmit="@HandleLogin"> <DataAnnotationsValidator /> <div class="mb-3"> <label class="form-label">邮箱</label> <InputText @bind-Value="loginModel.Email" class="form-control" /> <ValidationMessage For="@(() => loginModel.Email)" /> </div> <div class="mb-3"> <label class="form-label">密码</label> <InputText type="password" @bind-Value="loginModel.Password" class="form-control" /> <ValidationMessage For="@(() => loginModel.Password)" /> </div> <button type="submit" class="btn btn-primary">登录</button> @if (loginFailed) { <p class="text-danger mt-2">凭证错误,请重试</p> } </EditForm> @code { private LoginModel loginModel = new(); private bool loginFailed; private async Task HandleLogin() { loginFailed = !await AuthService.Login(loginModel.Email, loginModel.Password); if (!loginFailed) { NavManager.NavigateTo("/data"); } } public class LoginModel { [Required] [EmailAddress] public string Email { get; set; } = string.Empty; [Required] public string Password { get; set; } = string.Empty; } }
数据组件(Data.razor)
@page "/data" @inject HttpClient HttpClient @inject AuthService AuthService @inject NavigationManager NavManager <h3>API返回数据</h3> @if (data == null) { <p>加载中...</p> } else { <pre>@System.Text.Json.JsonSerializer.Serialize(data, new System.Text.Json.JsonSerializerOptions { WriteIndented = true })</pre> } <button @onclick="@HandleLogout" class="btn btn-secondary mt-3">退出登录</button> @code { private object? data; protected override async Task OnInitializedAsync() { var token = await AuthService.GetToken(); if (string.IsNullOrEmpty(token)) { NavManager.NavigateTo("/login"); return; } try { data = await HttpClient.GetFromJsonAsync<object>("/data"); } catch (HttpRequestException ex) { if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { NavManager.NavigateTo("/login"); } } } private async Task HandleLogout() { await AuthService.Logout(); NavManager.NavigateTo("/login"); } }
方案2:Cookie跨域认证(如果API用Cookie认证)
如果你的API使用Cookie而非JWT,需要配置跨域Cookie共享:
1. API端CORS配置
builder.Services.AddCors(options => { options.AddPolicy("BlazorCors", policy => { policy.WithOrigins("https://localhost:5002") // 替换为Blazor应用地址 .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // 允许携带Cookie }); }); // 中间件中启用CORS(放在UseRouting之后,UseAuthorization之前) app.UseCors("BlazorCors");
2. API端Cookie配置
修改Cookie的SameSite属性以支持跨域:
builder.Services.ConfigureApplicationCookie(options => { options.Cookie.SameSite = SameSiteMode.None; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // 仅HTTPS环境生效 });
3. 客户端HttpClient配置
让HttpClient自动携带Cookie:
builder.Services.AddScoped(sp => { var handler = new HttpClientHandler { UseCookies = true, CookieContainer = new System.Net.CookieContainer() }; return new HttpClient(handler) { BaseAddress = new Uri("https://localhost:5001/") // API地址 }; });
关键注意事项
- API端CORS必须正确配置:确保允许Blazor应用的来源地址,JWT模式不需要
AllowCredentials,Cookie模式必须开启。 - HTTPS要求:跨域Cookie共享必须在HTTPS环境下测试(Localhost默认支持),否则
SameSite=None的Cookie会被浏览器拦截。 - 令牌过期处理:可在
AuthMessageHandler中添加令牌过期检测逻辑,自动跳转到登录页。
内容的提问来源于stack exchange,提问作者Robin Wilson
相关产品推荐
相关产品推荐

