.NET 8.0 Blazor Server对接独立Identity API认证方案咨询
实现方案
一、自定义AuthenticationStateProvider
不需要启用Blazor Server内置的Cookie认证,完全依赖独立API的认证结果维护Blazor端的认证状态,让AuthorizedView能感知登录状态变化。
1. 创建自定义认证状态提供者
using Microsoft.AspNetCore.Components.Authorization; using System.Security.Claims; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; public class ApiAuthStateProvider : AuthenticationStateProvider { private readonly ProtectedLocalStorage _protectedLocalStorage; private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity()); public ApiAuthStateProvider(ProtectedLocalStorage protectedLocalStorage) { _protectedLocalStorage = protectedLocalStorage; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { try { // 从加密的客户端存储读取认证信息 var storedUser = await _protectedLocalStorage.GetAsync<AuthUser>("auth-user"); if (storedUser.Success && storedUser.Value != null && !string.IsNullOrEmpty(storedUser.Value.AccessToken)) { // 用API返回的Claims构造认证用户 var identity = new ClaimsIdentity(storedUser.Value.Claims, "ApiAuth"); var user = new ClaimsPrincipal(identity); return new AuthenticationState(user); } } catch { // 读取失败时返回匿名身份 } return new AuthenticationState(_anonymous); } // 登录/登出后更新认证状态,通知Blazor组件刷新 public async Task UpdateAuthenticationState(AuthUser authUser) { ClaimsPrincipal user; if (authUser != null && !string.IsNullOrEmpty(authUser.AccessToken)) { var identity = new ClaimsIdentity(authUser.Claims, "ApiAuth"); user = new ClaimsPrincipal(identity); await _protectedLocalStorage.SetAsync("auth-user", authUser); } else { user = _anonymous; await _protectedLocalStorage.DeleteAsync("auth-user"); } NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); } // 快捷登出方法 public async Task Logout() => await UpdateAuthenticationState(null); } // 存储认证信息的模型,与API返回的令牌、Claims对应 public class AuthUser { public string AccessToken { get; set; } public string RefreshToken { get; set; } public List<Claim> Claims { get; set; } = new(); }
2. 注册服务
在Program.cs中注册自定义提供者和依赖服务:
builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthStateProvider>(); builder.Services.AddAuthorizationCore(); builder.Services.AddProtectedBrowserStorage(); // 注册你的ApiClient builder.Services.AddScoped<ApiClient>();
二、登录UI与认证流程
1. 登录组件实现(含2FA处理)
创建Login.razor组件,处理账号密码登录和2FA验证,避免存储明文账号密码:
@inject ApiClient ApiClient @inject ApiAuthStateProvider AuthStateProvider @inject NavigationManager NavigationManager <div class="login-form"> <input @bind="_username" placeholder="用户名" /> <input @bind="_password" type="password" placeholder="密码" /> <button @onclick="HandleLogin">登录</button> @if (_show2FaForm) { <div class="2fa-form"> <input @bind="_twoFactorCode" placeholder="2FA验证码" /> <button @onclick="HandleTwoFaSubmit">验证</button> </div> } </div> @code { private string _username; private string _password; private string _twoFactorCode; private bool _show2FaForm; // 存储2FA挑战的临时标识,而非明文账号密码 private string _twoFactorUserId; private string _twoFactorProvider; private async Task HandleLogin() { try { var loginResult = await ApiClient.LoginAsync(_username, _password); // 登录成功,更新认证状态 var authUser = new AuthUser { AccessToken = loginResult.AccessToken, RefreshToken = loginResult.RefreshToken, Claims = loginResult.Claims.ToList() }; await AuthStateProvider.UpdateAuthenticationState(authUser); NavigationManager.NavigateTo("/"); } catch (HttpRequestException ex) { if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { var responseContent = await ex.GetResponseContentAsync(); if (responseContent.Contains("TwoFactorRequired")) { // 解析API返回的2FA挑战数据(默认MapIdentityApi会返回这些信息) var challengeData = System.Text.Json.JsonSerializer.Deserialize<TwoFactorChallenge>(responseContent); _twoFactorUserId = challengeData.UserId; _twoFactorProvider = challengeData.Provider; _show2FaForm = true; } else { // 处理账号密码错误等其他未授权情况 } } } } private async Task HandleTwoFaSubmit() { try { // 调用带2FA码的登录接口 var loginResult = await ApiClient.LoginWithTwoFactorAsync(_twoFactorUserId, _twoFactorProvider, _twoFactorCode); var authUser = new AuthUser { AccessToken = loginResult.AccessToken, RefreshToken = loginResult.RefreshToken, Claims = loginResult.Claims.ToList() }; await AuthStateProvider.UpdateAuthenticationState(authUser); NavigationManager.NavigateTo("/"); } catch (Exception ex) { // 处理2FA验证码错误 } } // 对应API返回的2FA挑战响应模型 private class TwoFactorChallenge { public string UserId { get; set; } public string Provider { get; set; } public string Message { get; set; } } }
2. 扩展ApiClient支持2FA登录
在ApiClient中添加2FA登录方法,对接默认Identity API的登录端点:
public async Task<LoginResult> LoginWithTwoFactorAsync(string userId, string provider, string twoFactorCode) { var request = new { UserId = userId, Provider = provider, Code = twoFactorCode, RememberMe = false }; var response = await _httpClient.PostAsJsonAsync("/account/login", request); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<LoginResult>(); }
三、API调用自动携带令牌
修改ApiClient,每次调用前从加密存储读取令牌并添加到请求头:
public class ApiClient { private readonly HttpClient _httpClient; private readonly ProtectedLocalStorage _protectedLocalStorage; public ApiClient(HttpClient httpClient, ProtectedLocalStorage protectedLocalStorage) { _httpClient = httpClient; _protectedLocalStorage = protectedLocalStorage; } // 通用方法:添加认证令牌到请求头 private async Task AddAuthHeader() { var storedUser = await _protectedLocalStorage.GetAsync<AuthUser>("auth-user"); if (storedUser.Success && storedUser.Value != null) { _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", storedUser.Value.AccessToken); } } // 示例:调用受保护的API接口 public async Task<UserProfile> GetCurrentUserProfile() { await AddAuthHeader(); return await _httpClient.GetFromJsonAsync<UserProfile>("/api/user/profile"); } }
四、2FA安全优化
默认MapIdentityApi的登录端点在要求2FA时,会返回包含UserId和Provider的响应体,无需存储明文账号密码:
- 第一次登录返回401时,解析响应中的挑战标识
- 第二次调用登录端点时,携带
UserId、Provider和2FA码即可完成认证 - 若API未返回这些信息,可在WebApi中扩展登录端点的响应内容,补充必要的挑战数据
五、刷新令牌维持登录状态
为避免令牌过期导致用户频繁登录,在ApiClient中实现刷新令牌逻辑:
public async Task RefreshTokenAsync() { var storedUser = await _protectedLocalStorage.GetAsync<AuthUser>("auth-user"); if (storedUser.Success && storedUser.Value != null && !string.IsNullOrEmpty(storedUser.Value.RefreshToken)) { var refreshRequest = new { RefreshToken = storedUser.Value.RefreshToken }; var response = await _httpClient.PostAsJsonAsync("/account/refresh", refreshRequest); if (response.IsSuccessStatusCode) { var newTokenData = await response.Content.ReadFromJsonAsync<LoginResult>(); var updatedUser = storedUser.Value; updatedUser.AccessToken = newTokenData.AccessToken; updatedUser.RefreshToken = newTokenData.RefreshToken; await _protectedLocalStorage.SetAsync("auth-user", updatedUser); // 更新认证状态 var authStateProvider = _serviceProvider.GetRequiredService<ApiAuthStateProvider>(); await authStateProvider.UpdateAuthenticationState(updatedUser); } } }
内容的提问来源于stack exchange,提问作者bckspc
相关产品推荐
相关产品推荐

